Keras 神经网络秘籍(五)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

第十四章:端到端学习

在前几章中,我们学习了如何使用循环神经网络RNN)分析顺序数据(文本),以及如何使用卷积神经网络CNN)分析图像数据。

本章将介绍如何使用 CNN + RNN 组合来解决以下案例研究:

  • 手写文本识别

  • 从图像生成标题

此外,我们还将学习一种新的损失函数——连接主义时间分类CTC)损失,用于解决手写文本识别问题。

最后,我们将学习如何使用束搜索(beam search)来为生成的文本提出合理的替代方案,同时解决从图像生成标题的问题。

介绍

假设我们正在转录手写文本的图像。在这种情况下,我们既要处理图像数据,又要处理顺序数据(因为图像中的内容需要顺序转录)。

在传统分析中,我们通常会手工设计解决方案——例如:我们可能会在图像上滑动一个窗口(该窗口的大小大致与一个字符相当),使得窗口能够检测每个字符,然后输出它所检测到的字符,并且具有较高的置信度。

然而,在这种情况下,窗口的大小或我们滑动的窗口数量是由我们手工设计的——这就成为了特征工程(特征生成)问题。

更加端到端的方法是通过将图像传递给 CNN 来提取特征,然后将这些特征作为输入传递给 RNN 的各个时间步,以便在各个时间步提取输出。

因此,我们将使用 CNN 和 RNN 的组合,通过这种方式处理问题,我们不需要构建任何手工设计的特征,而是让模型自行调整 CNN 和 RNN 的最优参数。

连接主义时间分类(CTC)

在手写文本识别或语音转录的监督学习中,传统方法的一大限制是,我们需要提供图像中哪些部分包含某个字符的标签(在手写识别中),或哪个音频子段包含某个音素(多个音素组合形成一个单词发音)。

然而,在构建数据集时,为每个字符在图像中的位置或每个音素在语音转录中的位置提供真实标签是非常昂贵的,因为要转录的数据集可能包含成千上万的单词或数百小时的语音。

CTC 对于解决图像的不同部分与不同字符之间的映射问题非常有用。在这一节中,我们将学习 CTC 损失函数的原理。

解码 CTC

假设我们正在转录一个包含ab文本的图像。这个例子可能看起来像以下任意一种(字符ab之间的空格不同),而输出标签(地面真相)仍然是相同的ab

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/cf409f12-0d59-4815-8c13-0abc072eb181.png

在下一步,我们将这些例子分成多个时间步,如下所示(每个框代表一个时间步):

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/77d67303-323b-495a-a8c4-8170d6abca28.png

在前面的例子中,我们总共有六个时间步(每个单元格代表一个时间步)。

我们将从每个时间步预测输出,其中每个时间步的输出是整个词汇表的 softmax。

假设我们正在执行 softmax,让我们看一下ab的第一个图片中每个时间步的输出如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/1b3bac58-0c5a-48a2-a533-44f676518f0b.png

请注意,前图中的**-表示一个空格。此外,如果图像的特征通过双向 LSTM(或 GRU)传递,则第四和第五时间步的输出可以是b**——因为下一时间步中的信息也可以影响执行双向分析时的前一个时间步的输出。

在最后一步,我们将压缩所有在连续时间步中具有相同值的 softmax 输出。

上述结果导致我们最终的输出为:-a-b-(以此示例为准)。

如果地面真相是abb,我们将期望在两个b之间有一个**-,以避免连续的b**被压缩成一个。

计算 CTC 损失值

对于我们在上一节中解决的问题,假设我们有以下情形,其中给定时间步中某个字符出现的概率如图中圆圈所示(请注意,每个时间步从t0t5的概率和为 1):

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/e6ae3e2b-0198-4204-93e2-c3099b144404.png

然而,为了使计算更简洁,便于我们理解,假设地面真相是a而非ab,并且假设输出仅有三个时间步而不是六个。修改后的三个时间步的输出如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/f49d376c-e7ca-493c-9c01-8642c6ba3ff2.png

如果每个时间步的 softmax 值满足以下任一情形,我们可以得到a的地面真相:

每个时间步的输出时间步 1 的字符概率时间步 2 的字符概率时间步 3 的字符概率组合的概率最终概率
- - a0.80.10.10.8 x 0.1 x 0.10.008
- a a0.80.90.10.8 x 0.9 x 0.10.072
a a a0.20.90.10.2 x 0.9 x 0.10.018
- a -0.80.90.80.8 x 0.9 x 0.80.576
- a a0.80.90.10.8 x 0.9 x 0.10.072
a - -0.20.10.80.2 x 0.1 x 0.80.016
a a -0.20.90.80.2 x 0.9 x 0.80.144
总体概率0.906

从之前的结果中我们可以看到,获得地面真值 a 的总体概率为 0.906

CTC 损失是总体概率的负对数 = -log(0.906) = 0.04

注意,由于每个时间步中概率最高的字符组合表示地面真值a,因此 CTC 损失接近于零

手写文本识别

在这个案例研究中,我们将致力于将手写图像转录成文本,从而提取出图像中的文字

一个手写样本如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/900d721e-6a43-4de5-9ced-850bc6332af3.png

注意,在上图中,手写字符的长度各不相同,图像的尺寸不同,字符之间的间距各异,且图像的质量不同

在这一部分,我们将学习如何使用 CNN、RNN 和 CTC 损失函数结合起来进行手写示例的转录

准备就绪

我们将采用的手写示例转录策略如下:

  • 下载包含手写文本图像的图像:

    • 在与该案例研究相关的 GitHub 代码文件中提供了多个包含手写文本图像的数据集

    • 确保在获得图像的同时,也获取了与图像对应的地面真值

  • 将所有图像调整为相同的大小,比如 32 x 128 尺寸

  • 在调整图像大小时,我们还应确保图像的纵横比没有被扭曲:

    • 这可以确保图像不会因为原始图像被调整为 32 x 128 尺寸而显得模糊不清
  • 我们将调整图像大小,确保不扭曲纵横比,然后将每个图像叠加到不同的空白 32 x 128 图像上

  • 反转图像的颜色,使背景为黑色,手写内容为白色

  • 缩放图像,使其数值介于零和一之间

  • 预处理输出(地面真值):

    • 提取输出中的所有唯一字符

    • 为每个字符分配一个索引

    • 找到输出的最大长度,然后确保我们预测的时间步数超过输出的最大长度

    • 通过对地面真值进行填充,确保所有输出的长度相同

  • 将处理过的图像通过 CNN 进行处理,以便我们提取出形状为 32 x 256 的特征

  • 将从 CNN 中提取的特征传递到双向的 GRU 单元中,以便我们能够封装相邻时间步中的信息

  • 每个时间步的 256 个特征作为该时间步的输入

  • 将输出通过一个稠密层,该层的输出值与地面真值中的唯一字符总数相同(在 CTC 损失部分介绍中给出的示例中,填充值(-)也将是唯一字符之一——其中填充值 - 表示字符之间的空格,或图片空白部分的填充

  • 在每个 32 个时间步中提取 softmax 及其对应的输出字符

如何实现…

以下代码中的前述算法如下执行(代码文件在 GitHub 上作为Handwritten_text_recognition.ipynb可用):

  1. 下载并导入数据集。该数据集包含手写文本的图像及其对应的地面真值(转录)。

  2. 构建一个函数,调整图片大小而不扭曲其宽高比,并填充其余图片,使它们都具有相同的形状:

def extract_img(img):
     target = np.ones((32,128))*255
     new_shape1 = 32/img.shape[0]
     new_shape2 = 128/img.shape[1]
     final_shape = min(new_shape1, new_shape2)
     new_x = int(img.shape[0]*final_shape)
     new_y = int(img.shape[1]*final_shape)
     img2 = cv2.resize(img, (new_y,new_x ))
     target[:new_x,:new_y] = img2[:,:,0]
     target[new_x:,new_y:]=255
     return 255-target

在前述代码中,我们创建了一个空白图片(名为 target)。在下一步中,我们已经重塑了图片以保持其宽高比。

最后,我们覆盖了我们创建的空白图片的重新缩放图片,并返回了背景为黑色的图片(255-target)。

  1. 读取图片并将其存储在列表中,如下所示的代码中所示:
filepath = '/content/words.txt'
f = open(filepath)
import cv2
count = 0
x = []
y = []
x_new = []
chars = set()
for line in f:
     if not line or line[0]=='#':
         continue
     try:
         lineSplit = line.strip().split(' ')
         fileNameSplit = lineSplit[0].split('-')
         img_path = '/content/'+fileNameSplit[0]+'/'+fileNameSplit[0] + '-' +              fileNameSplit[1]+'/'+lineSplit[0]+'.png'
         img_word = lineSplit[-1]
         img = cv2.imread(img_path)
         img2 = extract_img(img)
         x_new.append(img2)
         x.append(img)
         y.append(img_word)
         count+=1
     except:
         continue

在前述代码中,我们提取了每个图片,并根据我们定义的函数进行了修改。输入和不同场景的修改示例:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/cb7b3958-fbd3-4d1c-b6cb-40a4cc1ec9be.png

  1. 提取输出中的唯一字符,如下所示:
import itertools
list2d = y
charList = list(set(list(itertools.chain(*list2d))))
  1. 创建输出地面真值,如下所示的代码中所示:
num_images = 50000

import numpy as np
y2 = []
input_lengths = np.ones((num_images,1))*32
label_lengths = np.zeros((num_images,1))
for i in range(num_images):
     val = list(map(lambda x: charList.index(x), y[i]))
     while len(val)<32:
         val.append(79)
     y2.append(val)
     label_lengths[i] = len(y[i])
     input_lengths[i] = 32

在前述代码中,我们将每个字符的索引存储到一个列表中。此外,如果输出的大小少于 32 个字符,我们会用 79 进行填充,79 表示空白值。

最后,我们还存储标签长度(在地面真值中)和输入长度(始终为 32)。

  1. 将输入和输出转换为 NumPy 数组,如下所示:
x = np.asarray(x_new[:num_images])
y2 = np.asarray(y2)
x= x.reshape(x.shape[0], x.shape[1], x.shape[2],1)
  1. 定义目标,如下所示:
outputs = {'ctc': np.zeros([32])}

我们初始化 32 个零,因为批量大小将为 32。对于批量大小中的每个值,我们期望损失值为零。

  1. 定义 CTC 损失函数如下:
def ctc_loss(args):
     y_pred, labels, input_length, label_length = args
     return K.ctc_batch_cost(labels, y_pred, input_length, label_length)

前述功能将预测值、地面真值(标签)和输入、标签长度作为输入,并计算 CTC 损失值。

  1. 定义模型,如下所示:
input_data = Input(name='the_input', shape = (32, 128,1), dtype='float32')

inner = Conv2D(32, (3,3), padding='same')(input_data)
inner = Activation('relu')(inner)
inner = MaxPooling2D(pool_size=(2,2),name='max1')(inner)
inner = Conv2D(64, (3,3), padding='same')(inner)
inner = Activation('relu')(inner)
inner = MaxPooling2D(pool_size=(2,2),name='max2')(inner)
inner = Conv2D(128, (3,3), padding='same')(input_data)
inner = Activation('relu')(inner)
inner = MaxPooling2D(pool_size=(2,2),name='max3')(inner)
inner = Conv2D(128, (3,3), padding='same')(inner)
inner = Activation('relu')(inner)
inner = MaxPooling2D(pool_size=(2,2),name='max4')(inner)
inner = Conv2D(256, (3,3), padding='same')(inner)
inner = Activation('relu')(inner)
inner = MaxPooling2D(pool_size=(4,2),name='max5')(inner)
inner = Reshape(target_shape = ((32,256)), name='reshape')(inner)

在前述代码中,我们正在构建 CNN,将具有 32 x 128 形状的图片转换为具有 32 x 256 形状的图片:

gru_1 = GRU(256, return_sequences = True, name = 'gru_1')(inner)
gru_2 = GRU(256, return_sequences = True, go_backwards = True, name = 'gru_2')(inner)
mix_1 = add([gru_1, gru_2])
gru_3 = GRU(256, return_sequences = True, name = 'gru_3')(inner)
gru_4 = GRU(256, return_sequences = True, go_backwards = True, name = 'gru_4')(inner)

到目前为止定义的模型的体系结构如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/ceeb523d-37f3-4c1d-9828-32dc1dddfe58.png

在前述代码中,我们将从 CNN 获取的特征传递到 GRU。如前面所示的定义的体系结构继续从所示的图形开始如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/3703ea67-229b-4f73-bc63-a71be57a06d0.png

在下面的代码中,我们将两个 GRU 的输出进行拼接,从而同时考虑双向 GRU 和普通 GRU 生成的特征:

merged = concatenate([gru_3, gru_4])

添加前面一层后的架构如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/d3b78853-dfea-445c-8be8-538b17c2b466.png

在下面的代码中,我们通过一个全连接层传递 GRU 输出的特征,并应用 softmax 得到 80 个可能值中的一个作为输出:

dense = TimeDistributed(Dense(80))(merged)
y_pred = TimeDistributed(Activation('softmax', name='softmax'))(dense)

模型的架构继续如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/1f69ad73-9c27-40b1-8720-e37833d6fce1.png

  1. 初始化 CTC 损失所需的变量:
from keras.optimizers import Adam
Optimizer = Adam()
labels = Input(name = 'the_labels', shape=[32], dtype='float32')
input_length = Input(name='input_length', shape=[1],dtype='int64')
label_length = Input(name='label_length',shape=[1],dtype='int64')
output = Lambda(ctc_loss, output_shape=(1,),name='ctc')([y_pred, labels, input_length, label_length])

在前面的代码中,我们提到 y_pred(预测的字符值)、实际标签、输入长度和标签长度是 CTC 损失函数的输入。

  1. 按如下方式构建并编译模型:
model = Model(inputs = [input_data, labels, input_length, label_length], outputs= output)
model.compile(loss={'ctc': lambda y_true, y_pred: y_pred}, optimizer = Optimizer)

请注意,我们传递给模型的输入有多个。CTC 计算如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/8ad595f7-6135-494d-8787-4979a3460b79.png

  1. 创建以下输入和输出向量:
x2 = 1-np.array(x_new[:num_images])/255
x2 = x2.reshape(x2.shape[0],x2.shape[1],x2.shape[2],1)
y2 = np.array(y2[:num_images])
input_lengths = input_lengths[:num_images]
label_lengths = label_lengths[:num_images]
  1. 在多个批次的图片上拟合模型,代码如下所示:
import random

for i in range(100):
     samp=random.sample(range(x2.shape[0]-100),32)
     x3=[x2[i] for i in samp]
     x3 = np.array(x3)
     y3 = [y2[i] for i in samp]
     y3 = np.array(y3)
     input_lengths2 = [input_lengths[i] for i in samp]
     label_lengths2 = [label_lengths[i] for i in samp]
     input_lengths2 = np.array(input_lengths2)
     label_lengths2 = np.array(label_lengths2)
     inputs = {
     'the_input': x3,
     'the_labels': y3,
     'input_length': input_lengths2,
     'label_length': label_lengths2,
     }
     outputs = {'ctc': np.zeros([32])}
     model.fit(inputs, outputs,batch_size = 32, epochs=1, verbose =2)

在前面的代码中,我们一次抽取 32 张图片,将它们转换为数组,并拟合模型以确保 CTC 损失为零。

请注意,我们排除了最后 100 张图片(在 x2 中),不将其作为输入传递给模型,以便测试模型在该数据上的准确性。

此外,我们多次遍历整个数据集,因为将所有图片加载到 RAM 并转换为数组很可能会导致系统崩溃,因为需要大量内存。

随着训练轮数的增加,训练损失如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/65786b6a-f385-4f80-a721-9e7111c729f5.png

  1. 使用以下代码预测测试图片在每个时间步的输出:
model2 = Model(inputs = input_data, outputs = y_pred)
pred = model2.predict(x2[-5].reshape(1,32,128,1))

pred2 = np.argmax(pred[0,:],axis=1)
out = ""
for i in pred2:
  if(i==79):
    continue
  else:
    out += charList[i]
plt.imshow(x2[k].reshape(32,128))
plt.title('Predicted word: '+out)
plt.grid('off')

在前面的代码中,如果在某个时间步预测的字符是 79 号字符,我们将丢弃该输出:

测试示例及其对应的预测(标题中)如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/3841110b-83d4-4e17-a9ed-1a43606a7f33.png

图像字幕生成

在之前的案例研究中,我们学习了如何将 CNN、RNN 和 CTC 损失一起使用,以转录手写数字。

在本案例研究中,我们将学习如何将 CNN 和 RNN 架构结合起来,以为给定图片生成字幕。

这里是图片的一个样本:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/d236b475-d8ec-405c-bc4a-712208d8b564.png

  • 一位穿红色裙子的女孩,背景是圣诞树

  • 一位女孩正在展示圣诞树

  • 一位女孩正在公园里玩耍

  • 一位女孩正在庆祝圣诞节

准备就绪

在本节中,让我们列出转录图片的策略:

  • 我们将致力于通过处理一个包含图片和与之相关的描述的数据集,生成图片的字幕。包含图片及其对应字幕的数据集链接会在 GitHub 上的相关笔记本中提供。

  • 我们将提取每张图片的 VGG16 特征。

  • 我们还将对字幕文本进行预处理:

    • 将所有单词转换为小写

    • 移除标点符号

    • 为每个字幕添加开始和结束标记

  • 只保留狗或女孩的图片(我们执行此分析仅仅是为了加速模型训练,因为即使使用 GPU,运行此模型也需要大约 5 小时)。

  • 为字幕词汇表中的每个唯一单词分配索引。

  • 填充所有字幕(每个单词由索引值表示),以确保所有字幕现在都是相同的大小。

  • 为了预测第一个词,模型应将 VGG16 特征与开始标记的嵌入组合起来作为输入。

  • 类似地,为了预测第二个词,模型将采用 VGG16 特征和开始标记及第一个词的嵌入组合。

  • 以类似方式,我们继续获取所有预测的词。

  • 我们继续执行前面的步骤,直到预测到结束标记。

如何实现…

我们将编写之前定义的策略,如下所示(代码文件可在 GitHub 上找到 Image_captioning.ipynb):

  1. 下载并导入一个包含图像及其相应字幕的数据集。推荐的数据集可在 GitHub 上找到。

  2. 导入相关包,如下所示:

import glob
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
import pickle
from tqdm import tqdm
import pandas as pd
from keras.preprocessing import sequence
from keras.models import Sequential
from keras.layers import LSTM, Embedding, TimeDistributed, Dense, RepeatVector, merge, Activation, Flatten
from keras.optimizers import Adam, RMSprop
from keras.layers.wrappers import Bidirectional
from keras.applications.inception_v3 import InceptionV3
from keras.preprocessing import image
import nltk
  1. 加载字幕数据集,如以下代码所示:
caption_file = '...'
captions = open(caption_file, 'r').read().strip().split('\n')
d = {}
for i, row in enumerate(captions):
     row = row.split('\t')
     row[0] = row[0][:len(row[0])-2]
     if row[0] in d:
         d[row[0]].append(row[1])
     else:
         d[row[0]] = [row[1]]
total_images = list(d.keys())
  1. 加载图片并存储 VGG16 特征:
image_path = '...'
from keras.applications.vgg16 import VGG16
vgg16=VGG16(include_top=False, weights='imagenet', input_shape=(224,224,3))

import cv2
x = []
y = []
x2 = []
tot_images = ''
for i in range(len(d.keys())):
     for j in range(len(d[total_images[i]])):
         img_path = image_path+total_images[i]
         img = cv2.imread(img_path)
         try:
             img2 = cv2.resize(img, (224, 224))/255
             img3 = vgg16.predict(img2.reshape(1,224,224,3))
             x.append(img3)
             y.append(d[total_images[i]][j])
             tot_images = tot_images + ' '+total_images[i]
         except:
             continue
  1. 将 VGG16 特征转换为 NumPy 数组:
x = np.array(x)
x = x.reshape(x.shape[0],7,7,512)
  1. 创建一个函数,移除字幕中的标点符号,并将所有单词转换为小写:
def preprocess(text):
     text=text.lower()
     text=re.sub('[⁰-9a-zA-Z]+',' ',text)
     words = text.split()
     words2 = words
     words4=' '.join(words2)
     return(words4)

在以下代码中,我们预处理所有字幕并附加开始和结束标记:

caps = []
for key, val in d.items():
     if(key in img_path2):
         for i in val:
             i = preprocess(i)
             caps.append('<start> ' + i + ' <end>')
  1. 只附加属于儿童或狗的图片:
caps2 = []
x2 = []
img_path3 = []
for i in range(len(caps)):
     if (('girl') in caps[i]):
         caps2.append(caps[i])
         x2.append(x[i])
         img_path2.append(img_path[i])
     elif 'dog' in caps[i]:
         caps2.append(caps[i])
         x2.append(x[i])
         img_path2.append(img_path[i])
     else:
         continue
  1. 提取字幕中的所有唯一单词,如下所示:
words = [i.split() for i in caps2]
unique = []
for i in words:
     unique.extend(i)
unique = list(set(unique))
vocab_size = len(unique)
  1. 为词汇表中的单词分配索引,如以下代码所示:
word2idx = {val:(index+1) for index, val in enumerate(unique)}
idx2word = {(index+1):val for index, val in enumerate(unique)}
  1. 确定字幕的最大长度,以便我们将所有字幕填充到相同的长度:
max_len = 0
for c in caps:
     c = c.split()
     if len(c) > max_len:
         max_len = len(c)
  1. 将所有字幕填充到相同的长度,如下所示:
n = np.zeros(vocab_size+1)
y = []
y2 = []
for k in range(len(caps2)):
     t= [word2idx[i] for i in caps2[k].split()]
     y.append(len(t))
     while(len(t)<max_len):
         t.append(word2idx['<end>'])
     y2.append(t)
  1. 构建一个模型,该模型以图片为输入并从中创建特征:
from keras.layers import Input
embedding_size = 300
inp = Input(shape=(7,7,512))
inp1 = Conv2D(512, (3,3), activation='relu')(inp)
inp11 = MaxPooling2D(pool_size=(2, 2))(inp1)
inp2 = Flatten()(inp11)
img_emb = Dense(embedding_size, activation='relu') (inp2)
img_emb2 = RepeatVector(max_len)(img_emb)
  1. 构建一个模型,该模型以字幕为输入并从中创建特征:
inp2 = Input(shape=(max_len,))
cap_emb = Embedding((vocab_size+1), embedding_size, input_length=max_len) (inp2)
cap_emb2 = LSTM(256, return_sequences=True)(cap_emb)
cap_emb3 = TimeDistributed(Dense(300)) (cap_emb2)
  1. 将两个模型连接起来,并对所有可能的输出词进行 softmax 概率计算:
final1 = concatenate([img_emb2, cap_emb3])
final2 = Bidirectional(LSTM(256, return_sequences=False))(final1)
final3 = Dense(vocab_size+1)(final2)
final4 = Activation('softmax')(final3)

final_model = Model([inp, inp2], final4)
  1. 编译模型,如下所示:
from keras.optimizers import Adam
adam = Adam(lr = 0.0001)
final_model.compile(loss='categorical_crossentropy', optimizer = adam, metrics=['accuracy'])
  1. 拟合模型,如以下代码所示:
for i in range(500):
     x3 = []
     x3_sent = []
     y3 = []
     shortlist_y = random.sample(range(len(y)-100),32)
     for j in range(len(shortlist_y)):
         for k in range(y[shortlist_y[j]]-1):
             n = np.zeros(vocab_size+1) 
             x3.append(x2[shortlist_y[j]])
             pad_sent = pad_sequences([y2[shortlist_y[j]][:(k+1)]], maxlen=max_len, padding='post')
             x3_sent.append(pad_sent)
             n[y2[shortlist_y[j]][(k+1)]] = 1
             y3.append(n)
             x3 = np.array(x3)
             x3_sent =np.array(x3_sent)
             x3_sent = x3_sent.reshape(x3_sent.shape[0], x3_sent.shape[2])
             y3 = np.array(y3) 
             final_model.fit([x3/12, x3_sent], y3, batch_size = 32, epochs = 3, verbose = 1)

在前面的代码中,我们以每次 32 张图片的速度遍历所有图片。此外,我们正在创建输入数据集,方式是将字幕中的前 n 个输出词与图片的 VGG16 特征一起作为输入,而相应的输出则是字幕中的第 n+1^(th) 个词。

此外,我们将 VGG16 特征(x3)除以 12,因为我们需要将输入值缩放到 0 到 1 之间。

  1. 可以通过如下方式获取示例图片的输出字幕:
l=-25
im_path = image_path+ img_path3[l]
img1 = cv2.imread(im_path)
plt.imshow(img1)

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/4344dfc9-c830-4c95-abbe-a425403cb3d0.jpg

输出解码如下:

p = np.zeros(max_len)
p[0] = word2idx['<start>']
for i in range(max_len-1):
     pred= final_model.predict([x33[l].reshape(1,7,7,512)/12, p.reshape(1,max_len)])
     pred2 = np.argmax(pred)
     print(idx2word[pred2])
     p[i+1] = pred2
     if(idx2word[pred2]=='<end>'):
         break

上述代码的输出如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/6ef9c565-3bcb-4d8b-bc54-4776d13e1022.png

请注意,生成的字幕正确地检测到了狗是黑色的,并且正在跳跃。

生成字幕,使用束搜索

在前面关于字幕生成的部分,我们根据给定时间步中概率最高的单词进行了解码。在本节中,我们将通过使用束搜索来改进预测的字幕。

准备就绪

束搜索的工作原理如下:

  • 提取第一时间步中各种单词的概率(其中 VGG16 特征和开始标记是输入)

  • 我们不会仅提供最可能的单词作为输出,而是会考虑前三个最可能的单词。

  • 我们将进入下一个时间步,在该时间步提取前三个字符

  • 我们将循环遍历第一时间步的前三个预测,将其作为第二时间步预测的输入,并为每个可能的前三个输入预测提取前三个预测:

    • 假设abc是第一时间步的前三个预测

    • 我们将使用a作为输入,并结合 VGG16 特征来预测第二时间步中最可能的三个字符,同样地,对于bc也是如此。

    • 我们在第一时间步和第二时间步之间有九个输出组合。

    • 除了组合外,我们还将存储每个预测在所有九个组合中的置信度:

      • 例如:如果a在第一时间步的概率是 0.4,而x在第二时间步的概率是 0.5,那么组合的概率是 0.4 x 0.5 = 0.2
    • 我们将保留前三个组合,丢弃其他组合

  • 我们将重复前一步骤,筛选出前三个组合,直到达到句子的结尾。

三的值是我们在搜索组合时所用的束长度。

如何做到…

在本节中,我们将编写之前讨论过的束搜索策略的代码(代码文件可在 GitHub 中的Image_captioning.ipynb找到):

  1. 定义一个函数,接受图片的 VGG16 特征作为输入,连同来自前一步时间步的单词序列及其相应的置信度,并返回当前时间步的前三个预测:
beamsize = 3
def get_top3(img, string_with_conf):
     tokens, confidence = string_with_conf
     p = np.zeros((1, max_len))
     p[0, :len(tokens)] = np.array(tokens)
     pred = final_model.predict([img.reshape(1,7,7,512)/12, p])
     best_pred = list(np.argsort(pred)[0][-beamsize:])
     best_confs = list(pred[0,best_pred])
     top_best = [(tokens + list([best_pred[i]]), confidence*best_confs[i]) for i in range(beamsize)]
     return top_best

在前面的步骤中,我们将单词 ID 及其对应的置信度从string_with_conf参数中分离出来。此外,我们将令牌序列存储在数组中,并用它来进行预测。

在下一步中,我们提取下一个时间步的前三个预测,并将其存储在best_pred中。

此外,除了最好的单词 ID 预测,我们还会存储当前时间步内每个前三名预测的置信度。

最后,我们返回第二时间步的三个预测。

  1. 在句子的最大可能长度范围内循环,并提取所有时间步中的前三个可能的单词组合:
start_token = word2idx['<start>']
best_strings = [([start_token], 1)]
for i in range(max_len):
     new_best_strings = []
     for string in best_strings:
         strings = get_top3(x33[l], string)
         new_best_strings.extend(strings) 
         best_strings = sorted(new_best_strings, key=lambda x: x[1], reverse=True)[:beamsize]
  1. 遍历之前获得的best_strings并打印输出:
for i in range(3):
     string = best_strings[i][0]
     print('============')
     for j in string:
         print(idx2word[j])
         if(idx2word[j]=='<end>'):
             break

我们在上一节测试的相同图片的输出句子如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/ec9b27db-9d6e-46a2-9716-f0b4e19d595b.png https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/1448ba62-e00b-4a79-a24b-23d879f650c1.png https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/764e7e00-72e6-45cc-ae51-1fe3818710f2.png

注意,在这个特定的案例中,第一句和第二句在“jumping”和“playing”这两个词上有所不同,而第三句恰好和第一句相同,因为组合的概率要高得多。

第十五章:音频分析

在前几章中,我们学习了如何处理顺序文本数据。音频也可以看作是顺序数据,其振幅随时间变化。在这一章中,我们将学习以下内容:

  • 按类型分类歌曲

  • 使用深度学习生成音乐

  • 音频转录为文本

按类型分类歌曲

在这个案例研究中,我们将把一首歌分类为 10 种可能的类型之一。想象一下一个场景:我们被要求自动分类一首歌的类型,而不需要手动听它。这样,我们就能尽可能减少操作负担。

准备工作

我们将采用的策略如下:

  1. 下载一个包含各种音频录音及其对应类型的数据集。

  2. 可视化并对比不同类型音频信号的频谱图。

  3. 在频谱图上执行 CNN 操作:

    • 请注意,我们将在频谱图上执行 CNN 1D 操作,因为音频录音的情况下,翻译概念并不适用。
  4. 从 CNN 中提取特征,经过多次卷积和池化操作后。

  5. 展平输出并通过一个具有 10 个可能类别的输出层的密集层。

  6. 最小化类别交叉熵,以将音频录音分类为 10 个可能类别之一。

一旦我们对音频进行分类,我们将绘制每个音频输入的嵌入图,以便将相似的音频录音归为一类。这样,我们就能在不听歌的情况下识别新歌的类型,从而自动将音频输入分类到一个类型中。

如何操作…

上述策略的代码如下(代码文件在 GitHub 中名为Genre_classification.ipynb):

  1. 下载数据集并导入相关包:
import sys, re, numpy as np, pandas as pd, music21, IPython, pickle, librosa, librosa.dsiplay, os
from glob import glob
from tqdm import tqdm
from keras.utils import np_utils
  1. 循环遍历音频文件,提取输入音频的mel 频谱图特征,并存储音频输入的输出类型:
song_specs=[]
genres = []
for genre in os.listdir('...'): # Path to genres folder
  song_folder = '...' # Path to songs folder
  for song in os.listdir(song_folder):
    if song.endswith('.au'):
      signal, sr = librosa.load(os.path.join(song_folder, song), sr=16000)
      melspec = librosa.feature.melspectrogram(signal, sr=sr).T[:1280,]
      song_specs.append(melspec)
      genres.append(genre)
      print(song)
  print('Done with:', genre)

在上述代码中,我们加载音频文件并提取其特征。此外,我们还提取了信号的mel 频谱图特征。最后,我们将mel特征存储为输入数组,将类型存储为输出数组。

  1. 可视化频谱图:
plt.subplot(121)
librosa.display.specshow(librosa.power_to_db(song_specs[302].T),
 y_axis='mel',
 x_axis='time',)
plt.title('Classical audio spectrogram')
plt.subplot(122)
librosa.display.specshow(librosa.power_to_db(song_specs[402].T),
 y_axis='mel',
 x_axis='time',)
plt.title('Rock audio spectrogram')

以下是前述代码的输出:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/1d90d784-81f5-45d9-bbae-80908b53091b.png

你可以看到古典音频频谱图和摇滚音频频谱图之间有明显的区别。

  1. 定义输入和输出数组:
song_specs = np.array(song_specs)

song_specs2 = []
for i in range(len(song_specs)):
     tmp = song_specs[i]
     song_specs2.append(tmp[:900][:])
song_specs2 = np.array(song_specs2)

将输出类别转换为独热编码版本:

genre_one_hot = pd.get_dummies(genres)
  1. 创建训练和测试数据集:
x_train, x_test, y_train, y_test = train_test_split(song_specs2, np.array(genre_one_hot),test_size=0.1,random_state = 42)
  1. 构建并编译方法:
input_shape = (1280, 128)
inputs = Input(input_shape)
x = inputs
levels = 64
for level in range(7):
     x = Conv1D(levels, 3, activation='relu')(x)
     x = BatchNormalization()(x)
     x = MaxPooling1D(pool_size=2, strides=2)(x)
     levels *= 2
     x = GlobalMaxPooling1D()(x)
for fc in range(2):
     x = Dense(256, activation='relu')(x)
     x = Dropout(0.5)(x)
labels = Dense(10, activation='softmax')(x)

请注意,前述代码中的Conv1D方法与Conv2D非常相似;然而,Conv1D是一个一维滤波器,而Conv2D是一个二维滤波器:

model = Model(inputs=[inputs], outputs=[labels])
adam = keras.optimizers.Adam(lr=0.0001)
model.compile(loss='categorical_crossentropy',optimizer=adam,metrics=['accuracy'])
  1. 拟合模型:
history = model.fit(x_train, y_train,batch_size=128,epochs=100,verbose=1,validation_data=(x_test, y_test))

从前述代码中我们可以看到,模型在测试数据集上的分类准确率约为 60%。

  1. 从模型的倒数第二层提取输出:
from keras.models import Model
layer_name = 'dense_14'
intermediate_layer_model = Model(inputs=model.input,outputs=model.get_layer(layer_name).output)
intermediate_output = intermediate_layer_model.predict(song_specs2)

前面的代码在倒数第二层产生输出。

  1. 使用t-SNE将嵌入的维度减少到 2,这样我们就可以在图表上绘制我们的工作:
from sklearn.manifold import TSNE
tsne_model = TSNE(n_components=2, verbose=1, random_state=0)
tsne_img_label = tsne_model.fit_transform(intermediate_output)
tsne_df = pd.DataFrame(tsne_img_label, columns=['x', 'y'])
tsne_df['image_label'] = genres
  1. 绘制t-SNE输出:
from ggplot import *
chart = ggplot(tsne_df, aes(x='x', y='y', color='genres'))+ geom_point(size=70,alpha=0.5)
chart

以下是前面代码的图表:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/f0e76348-4592-4fb2-b376-87ec1d393faf.png

从前面的图中,我们可以看到相似类型的音频记录聚集在一起。这样,我们现在可以自动地将一首新歌分类到可能的某个类型中,而无需人工检查。然而,如果音频属于某个类型的概率不高,它可能会被送去人工复审,以确保错误分类的可能性较小。

使用深度学习生成音乐

在前一章中,我们学习了通过阅读小说来生成文本。在本节中,我们将学习如何通过一系列音频音符生成音频。

准备工作

一个 MIDI 文件通常包含音频文件中音符和和弦的信息,而音符对象包含音符的音高、八度和偏移量的信息。和弦对象包含同时演奏的一组音符。

我们将采用的音乐生成策略如下:

  • 提取音频文件中的音符

  • 为每个音符分配一个唯一的 ID。

  • 取 100 个历史音符的序列,第 101 个音符将是输出。

  • 训练 LSTM 模型。

如何做…

上述讨论的策略编码如下(代码文件可以在 GitHub 上的Music_generation.ipynb中找到),并附带推荐的音频文件:

  1. 导入相关的包和数据集:
!pip install mido music21
import mido, glob, os
from mido import MidiFile, MidiTrack, Message
import numpy as np
from music21 import converter, instrument, note, chord
from keras.utils import np_utils
from keras.layers import Input, LSTM, Dropout, Dense, Activation
from keras.models import Model

fname = '/content/nintendo.mid'
  1. 读取文件内容:
midi = converter.parse(fname)

前面的代码读取了一个分数流。

  1. 定义一个函数,读取分数流并提取其中的音符(如果音频文件中有静音,也会提取):
def parse_with_silence(midi=midi):
     notes = []
     notes_to_parse = None
     parts = instrument.partitionByInstrument(midi)
     if parts: # file has instrument parts
         notes_to_parse = parts.parts[0].recurse()
     else: # file has notes in a flat structure
         notes_to_parse = midi.flat.notes
     for ix, element in enumerate(notes_to_parse):
         if isinstance(element, note.Note):
             _note = str(element.pitch)
             notes.append(_note)
         elif isinstance(element, chord.Chord):
             _note = '.'.join(str(n) for n in element.normalOrder)
             notes.append(_note)
         elif isinstance(element, note.Rest):
             _note = '#'+str(element.seconds)
             notes.append(_note)
     return notes

在前面的代码中,我们通过遍历元素来获取音符,取决于元素是音符、和弦还是休止符(表示静音),我们提取相应的音符,附加它们,并返回附加后的列表。

  1. 从输入音频文件的流中提取音符:
notes = parse_with_silence()

一个示例音符输出如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/b3f25f4f-d8de-4fc1-bcae-dee7545cfe57.png

请注意,值以#开头表示静音(持续时间与紧邻#的数字相同)。

  1. 通过创建音符 ID 及其对应名称的字典,来创建输入和输出数据集:
# get all unique values in notes
pitchnames = sorted(set(item for item in notes))
# create a dictionary to map pitches to integers
note_to_int = dict((note, number) for number, note in enumerate(pitchnames))
network_input = []
network_output = []
    1. 创建输入和输出数组的序列:
sequence_length = 100
for i in range(0, len(notes) - sequence_length, 1):
     sequence_in = notes[i:i + sequence_length]
     sequence_out = notes[i + sequence_length]
     network_input.append([note_to_int[char] for char in sequence_in])
     network_output.append(note_to_int[sequence_out])

在前面的步骤中,我们将 100 个音符的序列作为输入,并提取第 101 个时间步的输出。

此外,我们还将音符转换为其对应的 ID:

n_patterns = len(network_input)
# reshape the input into a format compatible with LSTM layers
network_input = np.reshape(network_input, (n_patterns, sequence_length, 1))
# normalize input
network_input = network_input / np.max(network_input)
network_output = np_utils.to_categorical(network_output)

N = 9 * len(network_input)//10
print(network_input.shape, network_output.shape)
# (36501, 100, 1) (36501, 50)

在前面的代码中,我们正在重新调整输入数据的形状,以便将其馈送到 LSTM 层(该层需要batch_size形状、时间步数和每个时间步的特征数)。

此外,我们正在对输入进行归一化,并将输出转换为一组独热编码的向量。

  1. 拟合模型:
model.fit(network_input, network_output, epochs=100, batch_size=32, verbose = 1)

以下是前述代码的输出:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/3ee599ca-17c2-42a4-962e-f5fb91653bdd.png

  1. 生成预测:
from tqdm import trange
print('generating prediction stream...')
start = np.random.randint(0, len(network_input)-1)
int_to_note = dict((number, note) for number, note in enumerate(pitchnames))
pattern = network_input[start].tolist()
prediction_output = []

注意,在前面的代码中,我们选择了一个随机的音频位置,从那里我们将采样一个序列,用作未来时间步预测的种子。

  1. 通过一次处理 100 个音符的序列,生成下一个预测,将其附加到输入序列中,再生成下一个预测(通过获取最后 100 个音符的最新序列):
for note_index in trange(500):
     prediction_input = np.reshape(pattern, (1, len(pattern), 1))
     prediction = model.predict(prediction_input, verbose=0)
     index = np.argmax(prediction)
     result = int_to_note[index]
     prediction_output.append(result)
     pattern.append([index/49])
     pattern = pattern[1:len(pattern)]

注意,我们将索引(即模型的预测输出)除以 49,就像在构建模型时一样(除以np.max(network_input))。

上面的练习与文本生成练习略有不同,后者是基于输入词 ID 进行嵌入操作,而在这种情况下,我们没有进行嵌入。模型仍然在没有嵌入的情况下运行,可能是因为输入中唯一的值较少。

  1. 根据模型生成的值创建音符值:
offset = 0
output_notes = []

# create note and chord objects based on the values generated by the model
print('creating notes and chords')
for pattern in prediction_output:

    # pattern is a chord
    if (('.' in pattern) or pattern.isdigit()) and pattern[0]!='#':
        notes_in_chord = pattern.split('.')
        notes = []
        for current_note in notes_in_chord:
            new_note = note.Note(int(current_note))
            new_note.storedInstrument = instrument.Piano()
            notes.append(new_note)
        new_chord = chord.Chord(notes)
        new_chord.offset = offset
        output_notes.append(new_chord)

    # pattern is a note
    elif pattern[0]!='#':
        new_note = note.Note(pattern)
        new_note.offset = offset
        new_note.storedInstrument = instrument.Piano()
        output_notes.append(new_note)

    # pattern is silence
    else:
        new_note = note.Rest()
        new_note.offset = offset
        new_note.storedInstrument = instrument.Piano()
        new_note.quarterLength = float(pattern[1:])
        output_notes.append(new_note)
    # increase offset each iteration so that notes do not stack
    offset += 0.5

注意,在前面的代码中,我们将每个音符的时间偏移了 0.5 秒,这样在生成输出时音符不会重叠。

  1. 将生成的预测写入音乐流:
from music21 import stream
midi_stream = stream.Stream(output_notes)
midi_stream.write('midi', fp='OP.mid')

现在,你应该能够听到你的模型生成的音乐。

将音频转录为文本

在第十四章,端到端学习中,我们学习了如何将手写文本图像转录为文本。在这一部分,我们将利用类似的端到端模型将语音转录为文本。

准备中

我们将采用的语音转录策略如下:

  • 下载一个包含音频文件及其对应转录(实际结果)的数据集。

  • 在读取音频文件时指定采样率:

    • 如果采样率为 16,000,我们将从每秒音频中提取 16,000 个数据点。
  • 提取音频数组的快速傅里叶变换:

    • FFT 确保我们仅保留信号中最重要的特征。

    • 默认情况下,FFT 给我们提供n/2个数据点,其中n是整个音频录音中的数据点数量。

  • 对音频进行 FFT 特征采样,每次提取 320 个数据点;也就是说,我们每次提取 20 毫秒(320/16000 = 1/50 秒)的音频数据。

  • 此外,我们将在 10 毫秒间隔处每次采样 20 毫秒的数据。

  • 在这个练习中,我们将处理一个音频录音,其最大时长为 10 秒。

  • 我们将把 20 毫秒的音频数据存储到一个数组中:

    • 我们已经看到,每 10 毫秒采样 20 毫秒的数据。

    • 因此,对于一个一秒钟的音频片段,我们将有 100 x 320 个数据点,而对于一个 10 秒钟的音频片段,我们将有 1,000 x 320 = 320,000 个数据点。

  • 我们将初始化一个 160,000 个数据点的空数组,并用 FFT 值覆盖这些值——因为我们已经知道 FFT 值是原始数据点数量的一半

  • 对于每个 1,000 x 320 的数据点数组,我们将存储对应的转录文本

  • 我们将为每个字符分配一个索引,然后将输出转换为索引列表

  • 此外,我们还将存储输入长度(即预定义的时间步数)和标签长度(即输出中实际的字符数量)

  • 此外,我们将定义基于实际输出、预测输出、时间步数(输入长度)和标签长度(输出字符数)的 CTC 损失函数

  • 我们将定义一个模型,结合使用conv1D(因为这是音频数据)和 GRU

  • 此外,我们将确保通过批量归一化来规范化数据,以防止梯度消失

  • 我们将在数据批次上运行模型,在此过程中我们随机采样数据批次并将其输入模型,模型试图最小化 CTC 损失

  • 最后,我们将通过使用ctc_decode方法解码模型在新数据点上的预测

如何做到…

上述策略的代码实现如下(代码文件可在 GitHub 的Voice transcription.ipynb中找到):

  1. 下载数据集并导入相关的包:
$wget http://www.openslr.org/resources/12/train-clean-100.tar.gz
$tar xzvf train-clean-100.tar.gz

import librosa
import numpy as np
import pandas as pd
  1. 读取所有文件名及其对应的转录文本,并将它们转换为单独的列表:
import os, numpy as np
org_path = '/content/LibriSpeech/train-clean-100/'
count = 0
inp = []
k=0
audio_name = []
audio_trans = []
for dir1 in os.listdir(org_path):
     dir2_path = org_path+dir1+'/'
     for dir2 in os.listdir(dir2_path):
     dir3_path = dir2_path+dir2+'/'

     for audio in os.listdir(dir3_path):
         if audio.endswith('.txt'):
             k+=1
             file_path = dir3_path + audio
             with open(file_path) as f:
                 line = f.readlines()
                 for lines in line:
                     audio_name.append(dir3_path+lines.split()[0]+'.flac')
                     words2 = lines.split()[1:]
                     words4=' '.join(words2)
                     audio_trans.append(words4)
  1. 将转录文本的长度存储到一个列表中,这样我们就能理解最大转录文本的长度:
import re
len_audio_name=[]
for i in range(len(audio_name)):
     tmp = audio_trans[i]
     len_audio_name.append(len(tmp))
  1. 对于本次练习,为了能够在单个 GPU 上训练模型,我们将对前 2,000 个音频文件进行操作,这些音频文件的转录文本长度少于 100 个字符:
final_audio_name = []
final_audio_trans = []
for i in range(len(audio_name)):
     if(len_audio_name[i]<100):
         final_audio_name.append(audio_name[i])
         final_audio_trans.append(audio_trans[i])

在前面的代码中,我们仅为那些转录文本长度少于 100 个字符的音频录音存储音频名称和对应的转录文本

  1. 将输入存储为二维数组,并仅对那些时长少于 10 秒的音频文件存储对应的输出:
inp = []
inp2 = []
op = []
op2 = []
for j in range(len(final_audio_name)):
     t = librosa.core.load(final_audio_name[j],sr=16000, mono= True) 
     if(t[0].shape[0]<160000):
         t = np.fft.rfft(t[0])
         t2 = np.zeros(160000)
         t2[:len(t)] = t
         inp = []
         for i in range(t2.shape[0]//160):
             inp.append(t2[(i*160):((i*160)+320)])
             inp2.append(inp)
             op2.append(final_audio_trans[j])
  1. 为数据中的每个唯一字符创建一个索引:
import itertools
list2d = op2
charList = list(set(list(itertools.chain(*list2d))))
  1. 创建输入和标签长度:
num_audio = len(op2)
y2 = []
input_lengths = np.ones((num_audio,1))*243
label_lengths = np.zeros((num_audio,1))
for i in range(num_audio):
     val = list(map(lambda x: charList.index(x), op2[i]))
     while len(val)<243:
         val.append(29)
     y2.append(val)
     label_lengths[i] = len(op2[i])
     input_lengths[i] = 243

注意,我们正在创建一个 243 的输入长度,因为模型的输出(我们将在后续步骤中构建)具有 243 个时间步。

  1. 定义 CTC 损失函数:
import keras.backend as K
def ctc_loss(args):
    y_pred, labels, input_length, label_length = args
    return K.ctc_batch_cost(labels, y_pred, input_length, label_length
  1. 定义模型:
input_data = Input(name='the_input', shape = (999,161), dtype='float32')
inp = BatchNormalization(name="inp")(input_data)
conv= Conv1D(filters=220, kernel_size = 11,strides = 2, padding='valid',activation='relu')(inp)
conv = BatchNormalization(name="Normal0")(conv)
conv1= Conv1D(filters=220, kernel_size = 11,strides = 2, padding='valid',activation='relu')(conv)
conv1 = BatchNormalization(name="Normal1")(conv1)
gru_3 = GRU(512, return_sequences = True, name = 'gru_3')(conv1)
gru_4 = GRU(512, return_sequences = True, go_backwards = True, name = 'gru_4')(conv1)
merged = concatenate([gru_3, gru_4])
normalized = BatchNormalization(name="Normal")(merged)
dense = TimeDistributed(Dense(30))(normalized)
y_pred = TimeDistributed(Activation('softmax', name='softmax'))(dense)
Model(inputs = input_data, outputs = y_pred).summary()
  1. 定义 CTC 损失函数的输入和输出参数:
from keras.optimizers import Adam
Optimizer = Adam(lr = 0.001)
labels = Input(name = 'the_labels', shape=[243], dtype='float32')
input_length = Input(name='input_length', shape=[1],dtype='int64')
label_length = Input(name='label_length',shape=[1],dtype='int64')
output = Lambda(ctc_loss, output_shape=(1,),name='ctc')([y_pred, labels, input_length, label_length])
  1. 构建并编译模型:
model = Model(inputs = [input_data, labels, input_length, label_length], outputs= output)
model.compile(loss={'ctc': lambda y_true, y_pred: y_pred}, optimizer = Optimizer, metrics = ['accuracy'])

模型摘要如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/4cdf1be1-cff4-4eb0-8b4b-1f5ce3b93a33.png

  1. 在从输入中采样的数据批次上拟合模型:
for i in range(2500):
     samp=random.sample(range(x2.shape[0]-25),32)
     batch_input=[inp2[i] for i in samp]
     batch_input = np.array(batch_input)
     batch_input = batch_input/np.max(batch_input)
     batch_output = [y2[i] for i in samp]
     batch_output = np.array(batch_output)
     input_lengths2 = [input_lengths[i] for i in samp]
     label_lengths2 = [label_lengths[i] for i in samp]
     input_lengths2 = np.array(input_lengths2)
     label_lengths2 = np.array(label_lengths2)
     inputs = {'the_input': batch_input,
             'the_labels': batch_output,
             'input_length': input_lengths2,
             'label_length': label_lengths2}
     outputs = {'ctc': np.zeros([32])} 
     model.fit(inputs, outputs,batch_size = 32, epochs=1, verbose =1)

在前面的代码中,我们正在循环并提取 2,500 次数据批次,规范化输入数据,并拟合模型

此外,我们进行了大量的训练周期,因为对于这个特定的数据集和模型组合,CTC 损失下降得很慢。

  1. 预测测试音频:
model2 = Model(inputs = input_data, outputs = y_pred)

k=-12
pred= model2.predict(np.array(inp2[k]).reshape(1,999,161)/100)

在前面的代码中,我们指定了一个模型(model2),它接受输入的测试数组并提取每个 243 个时间步骤中的模型预测结果。

此外,我们从输入数组的最后提取了第 12^(th)个元素的预测(请注意,我们在训练模型时排除了最后 25 个输入数据点)。此外,我们还像之前一样对其进行了预处理,将输入数据传递给模型训练过程。

  1. 解码新数据点上的预测:
def decoder(pred):
     pred_ints = (K.eval(K.ctc_decode(pred,[243])[0][0])).flatten().tolist()
     out = ""
     for i in range(len(pred_ints)):
         if pred_ints[i]<28:
         out = out+charList[pred_ints[i]]
     print(out)

在前面的代码中,我们使用ctc_decode方法解码了预测结果。或者,我们也可以像提取手写图像转录中的预测那样解码预测。最后,我们打印出了预测结果。

我们将能够通过调用之前定义的函数来解码预测结果:

decoder(pred)

其中一个预测的输出如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/06e5ae31-fcc7-4a45-893e-13d568a5ed0e.png

虽然前面的输出看起来像是胡言乱语,但它在语音上与实际的音频相似,具体如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/cd63b2fd-7610-4ec3-99d0-17b98214b6d8.png

还有更多……

我们可以进一步提高转录准确性的一些方法如下:

  • 在更多数据点上进行训练

  • 融入语言模型对输出进行模糊匹配,以便我们能纠正预测结果。

第十六章:强化学习

在前几章中,我们学习了将输入映射到目标—其中输入和输出值是已知的。在本章中,我们将学习强化学习,其中目标和操作环境是已知的,但没有输入或输出的映射。强化学习的工作原理是,我们通过开始时随机采取行动,逐步从生成的输入数据(状态中的行动)和输出数据(通过采取某些行动所获得的奖励)中学习,生成输入值(智能体的状态)和相应的输出值(智能体在状态中采取某些行动所获得的奖励)。

本章我们将覆盖以下内容:

  • 在一个模拟游戏中采取的最佳行动,且奖励为非负值

  • 在模拟游戏中的状态下采取的最佳行动

  • Q-learning 在玩《Frozen Lake》时最大化奖励

  • 深度 Q 学习来平衡一个推车杆

  • 深度 Q 学习来玩《Space Invaders》游戏

在一个模拟游戏中采取的最佳行动,且奖励为非负值

在本节中,我们将理解如何在模拟游戏中采取正确的行动。请注意,这个练习将主要帮助你掌握强化学习的工作原理。

准备工作

让我们定义一下我们在这个模拟环境中操作的环境。

你有三个盒子,两个玩家正在玩游戏。玩家 1 标记一个盒子为 1,玩家 2 标记一个盒子为 2。能够标记两个连续盒子的玩家获胜。

该游戏的空白棋盘如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/a6def8c7-0658-4359-b45a-8cae86613db7.png

对于我们刚刚定义的问题,只有玩家 1 有机会赢得游戏。玩家 1 赢得游戏的可能情境如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/129d43fc-ab45-4bc1-acc2-63b404e4cd47.png

从问题设置中来看,玩家 1 获胜的直观方式是选择中间的盒子。这样,无论玩家 2 选择哪个盒子,玩家 1 都会在下一步获胜。

虽然对于玩家 1 来说,第一步是直观的,但在接下来的章节中,我们将学习一个智能体如何自动找出最佳的第一步行动。

我们将采取的策略来解决这个问题如下:

  • 我们初始化一个空白棋盘

  • 玩家 1 随机选择一个盒子

  • 玩家 2 从剩下的两个盒子中随机选择一个

  • 根据玩家 1 剩下的盒子,我们更新玩家 1 的奖励:

    • 如果玩家 1 能够在两个连续盒子中放置 1,他将成为赢家,并获得 1 的奖励

    • 否则,玩家 1 将获得 0 的奖励

  • 重复前面的练习 100 次,每次进行游戏并为给定的动作序列存储奖励

  • 现在,我们将继续计算各种第一步行动的平均奖励

  • 在第一次移动中选择的框,经过 100 次迭代后,具有最高平均奖励的是玩家 1 的最佳首次行动。

如何操作…

上述策略的代码如下(代码文件可在 GitHub 上的Finding_optimal_policy.ipynb找到):

  1. 定义游戏环境和执行游戏的函数:
def play_game():
     empty_board = [0,0,0]
     move = []
     for step in range(3):
         index_to_choose = [i for i,j in enumerate(empty_board) if j==0]
         samp = random.sample(range(len(index_to_choose)), 1)[0] 
         if(step%2==0):
             empty_board[index_to_choose[samp]]=1
             move.append(index_to_choose[samp])
         else:
             empty_board[index_to_choose[samp]]=2 
     return(reward(empty_board), move[0])

在前面的代码中,我们初始化了一个空的棋盘,所有单元格值为零,并进行了一次名为samp的随机移动。玩家 1 先行,然后是玩家 2,接着是玩家 1。我们以这种方式填充空棋盘。

  1. 定义一个函数来计算游戏结束时的奖励:
def reward(empty_board):
     reward = 0
     if((empty_board[0]==1 & empty_board[1]==1) | (empty_board[1]==1 & empty_board[2]==1)):
         reward = 1
     else:
         reward = 0
     return reward
  1. 玩游戏100次:
rew = []
step = []
for i in range(100):
     r, move = play_game()
     rew.append(r)
     step.append(move) 
  1. 计算选择某一首次行动的奖励:
sub_list = [i for i,j in enumerate(step) if j==1]
final_reward = 0
count = 0
for i in sub_list:
     final_reward += rew[i]
     count+=1
final_reward/count

当你对多种首次行动选项重复运行前面的代码时,你会注意到当占据第二个方格时,平均奖励最高。

在模拟游戏中,状态下采取的最佳行动

在前面的场景中,我们考虑了一个简化的情况,即当目标达成时会获得奖励。在此场景中,我们将通过引入负奖励来使游戏更加复杂。然而,目标仍然不变:在给定环境中最大化奖励,该环境同时包含正奖励和负奖励。

准备就绪

我们正在处理的环境如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/77011aa8-e3e7-41cc-9bf4-452707112100.png

我们从包含S的单元格开始,目标是到达奖励为**+1**的单元格。为了最大化获得奖励的机会,我们将使用贝尔曼方程来计算前面网格中每个单元格的值,如下所示:

当前单元格的值 = 从当前单元格移动到下一个单元格的奖励 + 折扣因子 * 下一个单元格的值

此外,在当前问题中,除了奖励为**+1**的单元格外,移动到任何其他单元格的奖励都是0

折扣因子可以被视为从一个单元格移动到另一个单元格时所消耗的能量。因此,在当前问题设置中,远离奖励单元格的单元格值较低。

一旦我们计算出每个单元格的值,我们就会移动到具有所有可能移动的单元格中最高值的单元格。

我们将采用的计算每个单元格值的策略如下:

  • 初始化一个空棋盘。

  • 定义代理在一个单元格内可能采取的行动。

  • 定义代理在当前单元格内采取行动时所处的状态。

  • 计算当前状态的值,该值依赖于移到下一个状态的奖励以及下一个状态的值。

  • 基于之前的计算,更新当前状态的单元格值。

  • 此外,存储当前状态下采取的行动,以便移动到下一个状态。

  • 请注意,在初期迭代中,距离终点较远的格子的值保持为零,而与终点相邻的格子的值则上升。

  • 随着我们多次迭代前面的步骤,我们将能够更新格子的值,从而决定代理应该遵循的最优路径。

如何做到这一点…

在本节中,我们将编写在前一节中规划的策略(代码文件在 GitHub 上的Finding_optimal_policy.ipynb中可以找到):

  1. 初始化一个空的棋盘:
empty_board = [[0,0,0]
             ,[0,0,1]]
  1. 定义在不同状态下可以采取的行动——其中D代表向下移动,R代表向右,L代表向左,U代表向上:
state_actions = {(0,0):('D','R')
                 ,(0,1):('D','R','L')
                 ,(0,2):('D','L')
                 ,(1,0):('U','R')
                 ,(1,1):('L','U','R') 
                 }
  1. 定义一个函数,根据当前状态和在当前状态下采取的行动来提取下一个状态:
def get_next_state(curr_state, action):
     i,j = curr_state
     if action=='D':
         i = i+1
     elif action=='U':
         i = i-1
     elif action=='R':
         j = j+1
     elif action=='L':
         j = j-1
     else:
         print('unk')
     return((i,j))
  1. 初始化列表,用于附加状态、行动和奖励:
curr_state = (0,0)
state_action_reward = []
state = []
state_action = []
  1. 在一个回合中最多执行 100 次行动(一个回合是游戏的一次实例),在每个格子(状态)中随机采取行动,并根据移动到下一个状态的奖励以及下一个状态的值来计算当前状态的值。

重复以上练习100次迭代(回合/游戏),并计算每个格子的值:

for m in range(100):
     curr_state = (0,0)
     for k in range(100):
         reward = 0
         action = state_actions[curr_state][random.sample(range(len(state_actions[curr_state])),1)[0]]
         next_state = get_next_state(curr_state, action)

在前面的代码中,我们在一个状态中采取随机行动,然后计算该行动对应的下一个状态:

        state.append(curr_state)
        empty_board[curr_state[0]][curr_state[1]] = reward + empty_board[next_state[0]][next_state[1]]*0.9 
        empty_board[curr_state[0]][curr_state[1]])

在前面的代码中,我们正在更新一个状态的值:

        curr_state = next_state
        state_action.append(action)

        if(next_state==(1,2)):
             reward+=1
             break

前面的结果在所有格子中的最终状态值如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/695f027e-2888-4b08-b8ca-dba64348fc4e.png

根据前面的输出,代理可以在游戏开始时采取右侧的行动或向下的行动(代理从左上角开始)。然而,如果代理在第一步采取向下的行动,那么在下一步它更适合采取向右的行动,因为右侧的状态值高于当前状态上方的状态值。

还有更多…

假设环境(各个格子及其对应的奖励)如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/a7b18107-739a-4819-b47b-2a8998b62a92.png

在不同状态下可以采取的行动如下:

state_actions = {(0,0):('D','R')
                 ,(0,1):('D','R')
                 ,(1,0):('R')
                 ,(1,1):('R') 
                 }

在多次游戏迭代后,各个格子的状态值如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/96dfeacf-52d9-47f3-928c-275ff5b32948.png

从前面的结果来看,我们可以看到,代理在左上角采取向下的行动要比向右移动更好,因为下方格子的状态值高于起始格子上方的状态值。

使用 Q 学习最大化 Frozen Lake 游戏中的奖励

到目前为止,在前面的章节中,我们一直在给定的状态下采取随机行动。此外,我们还通过代码定义了环境,并计算了每一步的下一个状态、行动和奖励。在本节中,我们将利用 OpenAI 的 Gym 包来在 Frozen Lake 环境中进行导航。

准备就绪

冻结湖环境如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/2f4064cc-944d-4d94-ab94-0a5c8b963098.png

代理从S状态开始,目标是通过尽量避开H状态,最终到达G状态。

在前面的环境中,代理可以处于 16 个可能的状态中。此外,代理可以采取四个可能的动作(向上、向下、向右或向左)。

我们将定义一个 q 表格,其中有 16 行对应 16 种状态,4 列对应每种状态下可以采取的四个动作。

在上一节中,我们学习到:

在一个状态下采取的动作的价值 = 奖励 + 折扣因子 * 下一个状态中采取的最佳可能动作的价值

我们将修改前面的公式如下:

在一个状态下采取的动作的价值 = 在该状态下采取的动作的价值 + 1 * (奖励 + 折扣因子 * 下一个状态中采取的最佳可能动作的价值 - 在该状态下采取的动作的价值)

最后,我们将把 1 替换为学习率,这样状态中动作的值更新就不会发生剧烈变化。这类似于神经网络中使用学习率的效果。

在一个状态下采取的动作的价值 = 在该状态下采取的动作的价值 + 学习率 * (奖励 + 折扣因子 * 下一个状态中采取的最佳可能动作的价值 - 在该状态下采取的动作的价值)

根据前面的内容,我们现在可以更新 q 表格,以便能够识别在不同状态下可以采取的最佳动作。

我们将采用以下策略来解决这个案例研究:

  • 在 OpenAI 的 Gym 中注册环境

  • 初始化一个零数组 q 表格,形状为 16 x 4

  • 在选择给定状态下的动作时,采用探索与利用的平衡方法:

    • 到目前为止,我们仅仅是探索了可能的整体动作,因为我们在给定状态下随机选择了一个动作。

    • 在本节中,我们将探索初始的几次迭代,因为我们在游戏的前几个回合中并不确定应该采取的最佳动作。

    • 然而,随着我们对游戏了解的深入,我们将利用已经学到的关于可能采取的动作的知识,同时仍然会随机采取一些动作(随着回合数的增加,随机动作的频率会逐渐减少)。

  • 在每一回合中:

    • 根据我们是尝试探索还是利用,选择一个动作

    • 确定新的状态和奖励,并检查通过采取上一步选择的动作,游戏是否结束

    • 初始化学习率参数和折扣因子

    • 使用前面讨论的公式,通过更新 q 表格中在某一状态下采取的前一个动作的价值

    • 重复前面的步骤,直到游戏结束

  • 此外,重复前面的步骤,进行 1,000 场不同的游戏

  • 查看 q 表格,找出在给定状态下应采取的最佳动作

  • 根据 q 表格绘制代理在状态中采取动作的路径

如何实现……

在这一部分,我们将编写我们之前讨论的策略(代码文件在 GitHub 上作为Frozen_Lake_with_Q_Learning.ipynb提供):

  1. 导入相关包:
import gym
from gym import envs
from gym.envs.registration import register

Gym 是一个开发和比较强化学习算法的工具包。它支持教智能体从行走到玩游戏(如 Pong 和 Pinball)等所有任务。

更多关于 Gym 的信息可以在这里找到:gym.openai.com/

  1. 注册环境:
register(
 id = 'FrozenLakeNotSlippery-v1',
 entry_point = 'gym.envs.toy_text:FrozenLakeEnv',
 kwargs = {'map_name': '4x4', 'is_slippery':False},
 max_episode_steps = 100,
 reward_threshold = 0.8196)
  1. 创建环境:
env = gym.make('FrozenLakeNotSlippery-v1')
  1. 检查创建的环境:
env.render()

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/eb411224-67a2-44b0-921e-610d1061e23a.png

上述步骤呈现(打印)环境:

env.observation_space

上述代码提供了环境中的状态-动作对的数量。在我们的例子中,考虑到它是一个 4x4 的网格,我们总共有 16 个状态。因此,我们有 16 个观察。

env.action_space.n

上述代码定义了在环境中可以在某个状态下执行的动作数量:

env.action_space.sample()

上述代码从可能的动作集合中采样一个动作:

env.step(action)

上述代码执行动作并生成新的状态和该动作的奖励,标记游戏是否结束,并为步骤提供附加信息:

env.reset()

上述代码重置了环境,使得智能体回到起始状态。

  1. 初始化 q 表:
import numpy as np
qtable = np.zeros((16,4))

我们将其初始化为(16, 4)的形状,因为有 16 个状态,每个状态有 4 个可能的动作。

  1. 运行多个游戏回合:

初始化超参数:

total_episodes=15000
learning_rate=0.8
max_steps=99
gamma=0.95
epsilon=1
max_epsilon=1
min_epsilon=0.01
decay_rate=0.005

玩多个游戏回合:

rewards=[]
for episode in range(total_episodes):
    state=env.reset()
    step=0
    done=False
    total_rewards=0

在下面的代码中,我们定义了要采取的动作。如果eps(它是一个在 0 到 1 之间生成的随机数)小于 0.5,我们进行探索;否则,我们进行利用(即考虑 q 表中的最佳动作)

    for step in range(max_steps):
        exp_exp_tradeoff=random.uniform(0,1)        
        ## Exploitation:
        if exp_exp_tradeoff>epsilon:
            action=np.argmax(qtable[state,:])
        else:
            ## Exploration
            action=env.action_space.sample()

在下面的代码中,我们获取新的状态和奖励,并通过在给定步骤中采取动作来标记游戏是否结束:

        new_state, reward, done, _ = env.step(action)

在下面的代码中,我们根据在某个状态下采取的动作更新 q 表。此外,我们还在当前状态下采取动作后,使用新状态更新状态:

        qtable[state,action]=qtable[state,action]+learning_rate*(reward+gamma*np.max(qtable[new_state,:])-qtable[state,action])
        total_rewards+=reward
        state=new_state

在以下代码中,由于游戏已结束,我们将继续进行新的游戏回合。然而,我们确保更新随机性因子(eps),该因子用于决定我们是进行探索还是利用。

        if(done):
             break
        epsilon=min_epsilon+(max_epsilon-min_epsilon)*np.exp(decay_rate*episode)
        rewards.append(total_rewards)
  1. 一旦我们构建了 q 表,我们现在就可以部署智能体,让其根据 q 表建议的最优动作来操作:
env.reset()

for episode in range(1):
    state=env.reset()
    step=0
    done=False
    print("-----------------------")
    print("Episode",episode)
    for step in range(max_steps):
        env.render()
        action=np.argmax(qtable[state,:])
        print(action)
        new_state,reward,done,info=env.step(action)

        if done:
            #env.render()
            print("Number of Steps",step+1)
            break
        state=new_state

上述代码给出了智能体必须经过的最优路径,以到达最终目标。

深度 Q 学习平衡推车杆

在之前的部分,我们学习了如何基于 q 表的值采取行动。然而,得到最优值是一个耗时的过程,因为智能体需要多次游戏才能得到最优的 q 表。

在这一部分,我们将学习如何使用神经网络,这样我们就能比使用 Q 学习时更快地得到最优值。

准备工作

对于这个练习,我们将注册一个推车-杆环境,可能的行动是向右或向左移动,以保持杆的平衡。此外,推车的位置、推车速度、杆的角度和杆尖端的速度是我们关于状态的信息。

游戏的规则可以在此找到:gym.openai.com/envs/CartPole-v1/

杆通过一个不带驱动的关节连接到推车上,推车沿着一个无摩擦的轨道移动。该系统通过对推车施加+1 或-1 的力量来控制。摆杆从直立开始,目标是防止其倒下。每当杆保持直立时,都会提供+1 的奖励。当杆与竖直方向的夹角超过 15 度,或者推车离中心超过 2.4 个单位时,回合结束。

为了平衡推车-杆,我们将采用与上一部分相同的策略。然而,深度 Q 学习的不同之处在于,我们将使用神经网络来帮助我们预测代理需要采取的最佳行动。

我们训练神经网络的方式如下:

  • 我们将存储状态值、所采取的行动和获得的奖励的信息:

    • 如果游戏没有结束(未结束),奖励为 1,否则为 0。
  • 初始时,模型基于随机初始化的权重进行预测,其中模型的输出层有两个节点,分别对应两个可能行动的新状态值。

  • 新状态值将基于最大化新状态值的行动。

  • 如果游戏未结束,我们将通过当前状态的奖励与新状态的最大状态值和折扣因子的乘积之和来更新当前状态的值。

  • 我们现在将覆盖先前获得的更新后的当前状态值来更新行动的值:

    • 如果当前步骤采取的行动是错误的(即游戏结束),那么当前状态下该行动的值为 0。

    • 否则,当前步骤中目标的值为正数。

    • 这样,我们让模型自己找出该采取的正确行动。

    • 此外,我们可以认为当奖励为零时,行动是错误的。然而,由于我们无法确定当奖励为 1 时它是否为正确行动,因此我们只更新我们采取的行动,并保持新状态的值(如果我们采取另一个行动)不变。

  • 我们将状态值附加到输入数组,并且将采取某个行动时在当前状态下的值作为输出数组。

  • 我们拟合模型,最小化前述数据点的均方误差。

  • 最后,我们在逐渐增加的回合数中不断减少探索。

如何做到这一点…

我们将按如下方式编码我们之前讨论的策略(代码文件可在 GitHub 的Deep_Q_learning_to_balance_a_cart_pole.ipynb中找到):

  1. 创建环境并将动作大小和状态大小存储在变量中:
import gym 
env = gym.make('CartPole-v0') 
state_size = env.observation_space.shape[0] 
action_size = env.action_space.n

一个倒立摆环境如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/2a3a72ea-30b2-4b46-b545-908bc873ef5a.png

  1. 导入相关的包:
import numpy as np
import random
from keras.models import Sequential
from keras.layers import Dense
from keras.optimizers import Adam
from collections import deque
  1. 定义模型:
model=Sequential()
model.add(Dense(24,input_dim=state_size,activation='relu'))
model.add(Dense(24,activation='relu'))
model.add(Dense(2,activation='linear'))
model.compile(loss='mse',optimizer=Adam(lr=0.01))
  1. 定义需要附加的列表:
memory = deque(maxlen=2000)
gamma = 0.95 # discount rate
epsilon = 1.0 # exploration rate
epsilon_min = 0.01
epsilon_decay = 0.995
done = False
batch_size=32
  1. 定义一个函数来重玩游戏:
def replay(model, batch_size,epsilon):
    epsilon_min = 0.01
    epsilon_decay = 0.995
    minibatch = random.sample(memory, batch_size)
    for state, action, reward, next_state, done in minibatch:
        target = reward
        if not done:
            target = (reward + gamma *np.amax(model.predict(next_state)[0]))
        new_action_value = model.predict(state)
        new_action_value[0][action] = target
        model.fit(state,new_action_value, epochs=1, verbose=0)
    if epsilon > epsilon_min:
        epsilon *= epsilon_decay
    return model,epsilon

在前面的代码中,我们定义了一个函数,该函数接收神经网络模型、批次大小和 epsilon(表示我们是探索还是开发的参数)。我们获取一个大小为batch_size的随机样本。请注意,你将在下一步了解记忆结构(包括状态、动作、奖励和next_state)。如果游戏未结束,我们将更新采取的动作的奖励;否则,目标奖励将为 0(因为游戏结束时奖励为 0)。

此外,模型预测采取某个动作的价值(因为模型在输出层有 2 个节点,每个节点分别输出采取其中一个动作的结果)。该函数返回更新后的模型和探索/开发系数(epsilon)。

  1. 在多个回合中玩游戏,并附加代理获得的得分。此外,确保代理采取的行动是根据 epsilon 值由模型决定的:
episodes=200
maxsteps=200
score_list = []
for e in range(episodes):
    state = env.reset()
    state = np.reshape(state, [1, state_size])

在前面的代码中,我们总共进行了 200 个回合的游戏,并且在每回合开始时重置环境。此外,我们将状态重塑,以便可以传递给神经网络模型:

    for step in range(maxsteps):
        if np.random.rand()<=epsilon:
            action=env.action_space.sample()
        else:
            action = np.argmax(model.predict(state)[0])

在前面的步骤中,我们根据探索参数(epsilon)采取行动,在某些情况下我们采取随机行动(env.actionspace.sample()),而在其他情况下,我们利用模型的预测:

        next_state, reward, done, _ = env.step(action)
        reward = reward if not done else -10
        next_state = np.reshape(next_state, [1, state_size])
        memory.append((state, action, reward, next_state, done))

在前面的步骤中,我们执行了一个动作并提取了下一个状态、奖励以及游戏是否结束的信息。此外,如果游戏结束,我们将奖励值重置为-10(这意味着代理做出了错误的动作)。进一步地,我们提取下一个状态并将其附加到记忆中。这样,我们为模型创建了一个数据集,模型利用当前状态和奖励计算两个可能动作之一的奖励:

        state = next_state
        if done:
          print("episode: {}/{}, score: {}, exp prob: {:.2}".format(e, episodes, step, epsilon))
          score_list.append(step)
          break
        if len(memory) > batch_size:
          model,epsilon=replay(model, batch_size,epsilon)

在前面的代码中,如果游戏结束,我们将得分(游戏过程中采取的步数)附加到列表中;否则,我们更新模型。此外,只有当内存中的数据点数量达到预定义的批次大小时,我们才会更新模型。

随着训练轮数增加,得分的变化如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/c8b05e8e-9ac2-4b2c-94cc-ee1ca482baad.png

使用深度 Q 学习来玩 Space Invaders 游戏

在上一节中,我们使用深度 Q 学习来玩倒立摆游戏。在本节中,我们将利用深度 Q 学习来玩 Space Invaders,这是一个比倒立摆更复杂的环境。

Space Invaders 游戏的截图如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/61dfcc9b-3271-482d-aef3-0eaebddf9a93.png

来源: gym.openai.com/envs/SpaceInvaders-v0/

这个练习的目标是最大化单场游戏中获得的分数。

准备开始

我们将采用的策略是构建一个能够最大化分数的智能体,具体如下:

  • 初始化Space Invaders-Atari2600游戏环境。

  • 对图像帧进行预处理:

    • 移除那些不一定会影响动作预测的像素。

      • 例如,玩家位置下方的像素
    • 规范化输入图像。

    • 在将图像传递给神经网络模型之前调整图像大小

  • 按照 Gym 环境的要求堆叠帧

  • 让智能体在多个回合中玩游戏:

    • 在初期回合中,我们会有较高的探索度,而随着回合的增加,探索度逐渐衰减。

    • 在某一状态下需要采取的动作取决于探索系数的值。

    • 将游戏状态和对应的奖励(基于在该状态下采取的动作)存储在记忆中。

    • 根据前几个回合获得的奖励来更新模型。

如何实现…

我们之前讨论的策略编码如下:

  1. 下载包含《太空入侵者》游戏的 ROM,并安装retro包:
$ wget http://www.atarimania.com/roms/Roms.rar && unrar x Roms.rar && unzip Roms/ROMS.zip
$ pip3 install gym-retro
$ python3 -m retro.import ROMS/
  1. 创建环境并提取观察空间:
env=retro.make(game='SpaceInvaders-Atari2600')
env.observation_space
# Box(210,160,3)
  1. 构建一个处理帧(图像/《太空入侵者》游戏的截图)预处理的函数:
def preprocess_frame(frame):
     # Greyscale frame 
     gray = rgb2gray(frame)
     # Crop the screen (remove the part below the player)
     # [Up: Down, Left: right]
     cropped_frame = gray[8:-12,4:-12]
     # Normalize Pixel Values
     normalized_frame = cropped_frame/255.0
     # Resize
     preprocessed_frame = transform.resize(normalized_frame, [110,84])
     return preprocessed_frame 
  1. 构建一个根据状态堆叠帧的函数:
stack_size = 4 # We stack 4 frames
# Initialize deque with zero-images one array for each image
stacked_frames = deque([np.zeros((110,84), dtype=np.int) for i in range(stack_size)], maxlen=4)
def stack_frames(stacked_frames, state, is_new_episode):
     # Preprocess frame
     frame = preprocess_frame(state) 
     if is_new_episode:
         # Clear our stacked_frames
         stacked_frames = deque([np.zeros((110,84), dtype=np.int) for i in range(stack_size)], maxlen=4) 
         # Because we're in a new episode, copy the same frame 4x
         stacked_frames.append(frame)
         stacked_frames.append(frame)
         stacked_frames.append(frame)
         stacked_frames.append(frame) 
         # Stack the frames
         stacked_state = np.stack(stacked_frames, axis=2) 
     else:
         # Append frame to deque, automatically removes the oldest frame
         stacked_frames.append(frame)
         # Build the stacked state (first dimension specifies different frames)
         stacked_state = np.stack(stacked_frames, axis=2) 
     return stacked_state, stacked_frames
  1. 初始化模型的超参数:
### MODEL HYPERPARAMETERS
state_size = [110, 84, 4] # Our input is a stack of 4 frames hence 110x84x4 (Width, height, channels) 
action_size = env.action_space.n # 8 possible actions
learning_rate = 0.00025 # Alpha (aka learning rate)
### TRAINING HYPERPARAMETERS
total_episodes = 50 # Total episodes for training
max_steps = 50000 # Max possible steps in an episode
batch_size = 32 # Batch size
# Exploration parameters for epsilon greedy strategy
explore_start = 1.0 # exploration probability at start
explore_stop = 0.01 # minimum exploration probability 
decay_rate = 0.00001 # exponential decay rate for exploration prob
# Q learning hyperparameters
gamma = 0.9 # Discounting rate
### MEMORY HYPERPARAMETERS
pretrain_length = batch_size # Number of experiences stored in the Memory when initialized for the first time
memory_size = 1000000 # Number of experiences the Memory can keep
### PREPROCESSING HYPERPARAMETERS
stack_size = 4 # Number of frames stacked
### MODIFY THIS TO FALSE IF YOU JUST WANT TO SEE THE TRAINED AGENT
training = False
## TURN THIS TO TRUE IF YOU WANT TO RENDER THE ENVIRONMENT
episode_render = False
  1. 构建一个从总记忆中采样数据的函数:
memory = deque(maxlen=100000)
def sample(memory, batch_size):
     buffer_size = len(memory)
     index = np.random.choice(np.arange(buffer_size),
     size = batch_size,
     replace = False) 
     return [memory[i] for i in index]
  1. 构建一个返回智能体需要采取的动作的函数:
def predict_action(model,explore_start, explore_stop, decay_rate, decay_step, state, actions):
     exp_exp_tradeoff = np.random.rand()
     explore_probability = explore_stop + (explore_start - explore_stop) * np.exp(-decay_rate * decay_step)
     if (explore_probability > exp_exp_tradeoff):
         choice = random.randint(1,len(possible_actions))-1
         action = possible_actions[choice]
     else:
         Qs = model.predict(state.reshape((1, *state.shape)))
         choice = np.argmax(Qs)
         action = possible_actions[choice]
     return action, explore_probability
  1. 构建一个微调模型的函数:
def replay(agent,batch_size,memory):
     minibatch = sample(memory,batch_size)
     for state, action, reward, next_state, done in minibatch:
     target = reward
     if not done:
         target = reward + gamma*np.max(agent.predict(next_state.reshape((1,*next_state.shape)))[0])
     target_f = agent.predict(state.reshape((1,*state.shape)))
     target_f[0][action] = target
     agent.fit(state.reshape((1,*state.shape)), target_f, epochs=1, verbose=0)
 return agent
  1. 定义神经网络模型:
def DQNetwork():
     model=Sequential()
     model.add(Convolution2D(32,input_shape=(110,84,4),kernel_size=8, strides=4, padding='valid',activation='elu'))
     model.add(Convolution2D(64, kernel_size=4, strides=2, padding='valid',activation='elu'))
     model.add(Convolution2D(128, kernel_size=3, strides=2, padding='valid',activation='elu'))
     model.add(Flatten())
     model.add(Dense(units=512))
     model.add(Dense(units=3,activation='softmax'))
     model.compile(optimizer=Adam(0.01),loss='mse')
     return model

模型的总结如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/4a623396-afa7-491b-96da-6853a83906e6.jpg

  1. 循环多个回合并持续玩游戏,同时更新模型:
agent = DQNetwork()
agent.summary()
rewards_list=[]
Episodes=200
# Iterate the game
for episode in range(Episodes):
     # reset state in the beginning of each game
     step = 0
     decay_step = 0
     episode_rewards = []
     state = env.reset()
     state, stacked_frames = stack_frames(stacked_frames, state, True)
     while step < max_steps:
         step += 1
         decay_step +=1
         # Predict the action to take and take it
         action, explore_probability = predict_action(agent,explore_start, explore_stop, decay_rate, decay_step, state, possible_actions)
 #Perform the action and get the next_state, reward, and done information
         next_state, reward, done, _ = env.step(action)
         # Add the reward to total reward
         episode_rewards.append(reward)
     if done:
 # The episode ends so no next state
         next_state = np.zeros((110,84), dtype=np.int)
         next_state, stacked_frames = stack_frames(stacked_frames, next_state, False)
 # Set step = max_steps to end the episode
         step = max_steps
 # Get the total reward of the episode
         total_reward = np.sum(episode_rewards)
         print('Episode:{}/{} Score:{} Explore Prob:{}'.format(episode,Episodes,total_reward,explore_probability))
         rewards_list.append((episode, total_reward))
 # Store transition <st,at,rt+1,st+1> in memory D
         memory.append((state, action, reward, next_state, done))
     else:
 # Stack the frame of the next_state
         next_state, stacked_frames = stack_frames(stacked_frames, next_state, False)
 # Add experience to memory
         memory.append((state, action, reward, next_state, done))
 # st+1 is now our current state
         state = next_state
     env.render() 
 # train the agent with the experience of the episode
 agent=replay(agent,batch_size,memory)
  1. 绘制随着回合增加获得的奖励:
score=[]
episode=[]
for e,r in rewards_list:
     episode.append(e)
     score.append(r)
import matplotlib.pyplot as plt
plt.plot(episode,score)

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/3f3345e3-c0b6-49bc-9009-b2f000a06908.png

从中可以看出,模型已经学会在某些回合中得分超过 800 分。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值