TensorFlow Lite,ML Kit 和 Flutter 移动深度学习(二)
原文:Mobile Deep Learning with TensorFlow Lite, ML Kit and Flutter
五、从摄像机源生成实时字幕
作为人类,我们每天在不同的场景中看到一百万个物体。 对于人类来说,描述场景通常是一件微不足道的任务:我们所做的事情甚至都不需要花费大量的时间去思考。 但是,机器要理解图像或视频等视觉媒体中呈现给它的元素和场景是一项艰巨的任务。 但是,对于人工智能(AI)的几种应用,具有在计算机系统中理解此类图像的功能很有用。 例如,如果我们能够设计出可以将周围环境实时转换为音频的机器,则对视障人士将大有帮助。 此外,研究人员一直在努力实时生成图像和视频的字幕,以提高网站和应用上呈现的内容的可访问性。
本章介绍了一种使用摄像机供稿实时生成自然语言字幕的方法。 在此项目中,您将创建一个使用存储在设备上的自定义预训练模型的相机应用。 该模型使用深层卷积神经网络(CNN)和长短期记忆(LSTM)生成字幕。
我们将在本章介绍以下主题:
- 设计项目架构
- 了解图像字幕生成器
- 了解相机插件
- 创建相机应用
- 从相机源生成图像字幕
- 创建材质应用
让我们从讨论此项目将要遵循的架构开始。
设计项目架构
在这个项目中,我们将构建一个移动应用,当指向任何风景时,它将能够创建描述该风景的标题。 这样的应用对于有视觉缺陷的人非常有用,因为它既可以用作网络上的辅助技术,又可以与 Alexa 或 Google Home 等语音界面搭配使用,用作日常应用。 该应用将调用一个托管 API,该 API 将为传递给它的任何给定图像生成标题。 API 返回该图像的三个最佳字幕,然后该应用将其显示在应用中相机视图的正下方。
从鸟瞰图可以通过下图说明项目架构:
输入将是在智能手机中获得的相机提要,然后将其发送到托管为网络 API 的图像标题生成模型。 该模型在 Red Hat OpenShift 上作为 Docker 容器托管。 图像标题生成模型返回图像的标题,然后将其显示给用户。
有了关于如何构建应用的清晰思路,让我们首先讨论图像字幕的问题以及如何解决它们。
了解图像字幕生成器
计算机科学的一个非常流行的领域是图像处理领域。 它涉及图像的操纵以及我们可以从中提取信息的各种方法。 另一个流行的领域是自然语言处理(NLP),涉及如何制造可以理解和产生有意义的自然语言的机器。 图像标题定义了两个主题的混合,试图首先提取出现在任何图像中的对象的信息,然后生成描述对象的标题。
标题应以有意义的字串形式生成,并以自然语言句子的形式表示。
考虑下图:
图像中可以检测到的物体如下:勺子,玻璃杯,咖啡和桌子。
但是,我们对以下问题有答案吗?
- 杯子里装着咖啡还是汤匙,还是空的?
- 桌子在玻璃上方还是下方?
- 汤匙在桌子上方还是下方?
我们意识到,为了回答上述问题,我们需要使用如下语句:
- 杯子里装着咖啡。
- 玻璃放在桌子上。
- 汤匙放在桌子上。
因此,如果我们试图在图像周围创建标题,而不是简单地识别图像中的项目,我们还需要在可见项目之间建立一些位置和特征关系。 这将帮助我们获得良好的图像标题,例如一杯咖啡在桌子上,旁边放着勺子。 在图像标题生成算法中,我们尝试从图像创建此类标题。
但是,一个字幕可能并不总是足以描述风景,我们可能必须在两个可能相同的字幕之间进行选择,如以下屏幕截图所示:
Allef Vinicius 在 Unsplash 上的照片
您如何在前面的屏幕快照中描述图像?
您可以提出以下任何标题:
- 背景中有两棵树和多云的天空。
- 一把椅子和一把吉他放在地上。
根据用户,这提出了在任何图像中重要的问题。 尽管最近有一些设计用于处理这种情况的方法,例如“注意机制”方法,但在本章中我们将不对其进行深入讨论。
您可以在 CaptionBot 的这个页面上查看由 Microsoft 创建的图像字幕系统的非常酷的演示。
现在让我们定义将用于创建图像字幕模型的数据集。
了解数据集
不出所料,我们需要大量的通用图像以及可能列出的标题。 我们已经在上一节“了解图像字幕生成器”中显示,单个图像可以具有多个字幕,而不必任何一个都错了。 因此,在这个项目中,我们将研究 Flickr8k 数据集。 除此之外,我们还需要由 Jeffrey Pennington,Richard Socher 和 Christopher D. Manning 创建的 GloVE 嵌入。 简而言之,GloVE 告诉我们在给定单词之后可能跟随哪些单词,从而帮助我们从一组不连续的单词中形成有意义的句子。
您可以在这个页面上阅读有关 GloVE 嵌入的更多信息,以及描述它们的论文。
Flickr8k 数据集包含 8,000 个图像样本,以及每个图像的五个可能的标题。 还有其他可用于该任务的数据集,例如具有 30,000 个样本的 Flickr30k 数据集,或具有 180,000 张图像的 Microsoft COCO 数据集。 虽然使用较大的数据库会产生更好的结果,但是为了能够在普通机器上训练模型,我们将不再使用它们。 但是,如果可以使用高级计算能力,则可以肯定地尝试围绕较大的数据集构建模型。
您可以通过伊利诺伊大学厄本那香槟分校提供的以下格式的请求来下载 Flickr8k 数据集。
下载数据集时,您将能够看到以下文件夹结构:
Flickr8k/
- dataset
- images
- 8091 images
- text
- Flickr8k.token.txt
- Flickr8k.lemma.txt
- Flickr_8k.trainImages.txt
- Flickr_8k.devImages.txt
- Flickr_8k.testImages.txt
- ExpertAnnotations.txt
- CrowdFlowerAnnotations.txt
在可用的文本文件中,我们感兴趣的是Flickr8k.token.txt,其中包含dataset目录下images文件夹中每个图像的原始标题。
字幕以以下格式显示:
1007129816_e794419615.jpg#0 A man in an orange hat staring at something .
1007129816_e794419615.jpg#1 A man wears an orange hat and glasses .
1007129816_e794419615.jpg#2 A man with gauges and glasses is wearing a Blitz hat .
1007129816_e794419615.jpg#3 A man with glasses is wearing a beer can crocheted hat .
1007129816_e794419615.jpg#4 The man with pierced ears is wearing glasses and an orange hat .
通过检查,我们可以观察到前面示例中的每一行都包含以下部分:
Image_Filename#Caption_Number Caption
因此,通过浏览dataset/images文件夹中存在的图像的文件中的每一行,我们可以将标题映射到每个图像。
现在开始处理图像标题生成器代码。
建立图像字幕生成模型
在本节中,我们将看一看代码,这些代码将帮助我们创建一个管道,以将抛出该图像的图像转换为字幕。 我们将本节分为四个部分,如下所示:
- 初始化字幕数据集
- 准备字幕数据集
- 训练
- 测试
让我们从项目初始化开始。
初始化字幕数据集
在本节介绍的步骤中,我们将导入项目所需的模块并将数据集加载到内存中。 让我们从导入所需的模块开始,如下所示:
- 导入所需的库,如下所示:
import numpy as np
import pandas as pd
import nltk
from nltk.corpus import stopwords
import re
import string
import pickle
import matplotlib.pyplot as plt
%matplotlib inline
您会看到在这个项目中将使用许多模块和子模块。 在模型的运行中,它们都非常重要,从本质上讲,帮助器模块也是如此。 下一步,我们将导入更多特定于构建模型的模块。
- 导入 Keras 和子模块,如下所示:
import keras
from keras.layers.merge import add
from keras.preprocessing import image
from keras.utils import to_categorical
from keras.models import Model, load_model
from keras.applications.vgg16 import VGG16
from keras.preprocessing.sequence import pad_sequences
from keras.layers import Input, Dense, Dropout, Embedding, LSTM
from keras.applications.resnet50 import ResNet50, preprocess_input, decode_predictions
我们导入了 Keras 模块以及其他几个子模块和方法,以帮助我们快速构建深度学习模型。 Keras 是可用的最受欢迎的深度学习库之一,除 TensorFlow 外,还可以与 Theano 和 PyTorch 等其他框架一起使用。
- 加载字幕-在这一步中,我们将
Flickr8k.token.txt文件中存在的所有字幕加载到单个captions列表中,如下所示:
caption_file = "./data/Flickr8k/text/Flickr8k.token.txt"
captions = []
with open(caption_file) as f:
captions = f.readlines()
captions = [x.strip() for x in captions]
从文件加载所有标题后,让我们看看它们包含的内容,如下所示:
captions[:5]
正如预期的那样,并在前面的“了解数据集”部分中提到,我们获得了数据集中的以下前五行:
['1000268201_693b08cb0e.jpg#0\tA child in a pink dress is climbing up a set of stairs in an entry way .',
'1000268201_693b08cb0e.jpg#1\tA girl going into a wooden building .',
'1000268201_693b08cb0e.jpg#2\tA little girl climbing into a wooden playhouse .',
'1000268201_693b08cb0e.jpg#3\tA little girl climbing the stairs to her playhouse .',
'1000268201_693b08cb0e.jpg#4\tA little girl in a pink dress going into a wooden cabin .']
既然我们已经看到了写入每一行的模式,那么我们就可以继续分割每一行,以便可以将数据放入数据结构中,这比一大串字符串有助于更快地访问和更新。
准备字幕数据集
在以下步骤中,我们将处理加载的字幕数据集,并将其转换为适合对其进行训练的形式:
- 在此步骤中,我们将图像描述拆分并以字典格式存储,以方便将来的代码中使用,如以下代码块所示:
descriptions = {}
for x in captions:
imgid, cap = x.split('\t')
imgid = imgid.split('.')[0]
if imgid not in descriptions.keys():
descriptions[imgid] = []
descriptions[imgid].append(cap)
在前面的代码行中,我们将文件中的每一行细分为图像 ID 和每个图像标题的部分。 我们用它创建了一个字典,其中图像 ID 是字典键,每个键值对都包含五个标题的列表。
- 接下来,我们开始进行基本的字符串预处理,以便继续在字幕上应用自然语言技术,如下所示:
for key, caps in descriptions.items():
for i in range(len(caps)):
caps[i] = caps[i].lower()
caps[i] = re.sub("[^a-z]+", " ", caps[i])
- 另外,为了帮助我们将来分配合适的内存空间大小并准备词汇表,让我们创建标题文本中所有单词的列表,如下所示:
allwords = []
for key in descriptions.keys():
_ = [allwords.append(i) for cap in descriptions[key] for i in cap.split()]
- 一旦创建了所有单词的列表,就可以创建单词的频率计数。 为此,我们使用
collections模块的Counter方法。 一些单词在数据集中很少出现。 删除这些单词是一个好主意,因为它们不太可能频繁出现在用户提供的输入中,因此不会为字幕生成算法增加太多价值。 我们使用以下代码进行操作:
from collections import Counter
freq = dict(Counter(allwords))
freq = sorted(freq.items(), reverse=True, key=lambda x:x[1])
threshold = 15
freq = [x for x in freq if x[1]>threshold]
print(len(freq))
allwords = [x[0] for x in freq]
让我们通过运行以下代码来尝试查看最常用的单词:
freq[:10]
我们看到以下输出:
[('a', 62995),
('in', 18987),
('the', 18420),
('on', 10746),
('is', 9345),
('and', 8863),
('dog', 8138),
('with', 7765),
('man', 7275),
('of', 6723)]
我们可以得出结论,停用词在字幕文本中占很大比例。 但是,由于我们在生成句子时需要它们,因此我们不会将其删除。
训练
在以下步骤中,我们加载训练并测试图像数据集并对其进行训练:
- 现在,将分离的训练和测试文件加载到数据集中。 它们包含图像文件名列表,它们实际上是带有文件扩展名的图像 ID,如以下代码块所示:
train_file = "./data/Flickr8k/text/Flickr_8k.trainImages.txt"
test_file = "./data/Flickr8k/text/Flickr_8k.testImages.txt"
现在,我们将处理训练图像列表文件以提取图像 ID,并省略文件扩展名,因为在所有情况下它都相同,如以下代码片段所示:
with open(train_file) as f:
cap_train = f.readlines()
cap_train = [x.strip() for x in cap_train]
我们对测试图像列表进行相同的操作,如下所示:
with open(test_file) as f:
cap_test = f.readlines()
cap_test = [x.strip() for x in cap_test]
train = [row.split(".")[0] for row in cap_train]
test = [row.split(".")[0] for row in cap_test]
- 现在,我们将创建一个字符串,其中合并每个图像的所有五个可能的标题,并将它们存储在
train_desc中。 字典。 我们使用#START#和#STOP#区分字幕,以便将来在字幕生成中使用它们,如以下代码块所示:
train_desc = {}
max_caption_len = -1
for imgid in train:
train_desc[imgid] = []
for caption in descriptions[imgid]:
train_desc[imgid].append("#START# " + caption + " #STOP#")
max_caption_len = max(max_caption_len, len(caption.split())+1)
- 我们将使用 Keras 模型资源库中的
ResNet50预训练模型。 我们将输入形状设置为224 x 224 x 3,其中224 x 244是将传递给模型的每个图像的尺寸,而 3 是颜色通道的数量。 请注意,与美国国家混合标准技术研究院(MNIST)数据集不同,在该数据集中每个图像的尺寸均相等,而 Flickr8k 数据集则并非如此。 该代码可以在以下代码段中看到:
model = ResNet50(weights="imagenet", input_shape=(224,224,3))
model.summary()
从高速缓存中下载或加载模型后,将为每个层显示模型摘要。 但是,我们需要根据需要重新训练模型,因此我们将删除并重新创建模型的最后两层。 为此,我们使用与加载的模型相同的输入来创建一个新模型,并且输出等效于倒数第二层的输出,如以下代码片段所示:
model_new = Model(model.input, model.layers[-2].output)
- 我们将需要一个函数来重复预处理图像,预测图像中包含的特征,并根据图像中识别出的对象或属性形成特征向量。 因此,我们创建一个
encode_image函数,该函数接受图像作为输入参数,并通过ResNet50重新训练的模型运行图像,从而返回图像的特征向量表示,如下所示:
def encode_img(img):
img = image.load_img(img, target_size=(224,224))
img = image.img_to_array(img)
img = np.expand_dims(img, axis=0)
img = preprocess_input(img)
feature_vector = model_new.predict(img)
feature_vector = feature_vector.reshape((-1,))
return feature_vector
- 现在,我们需要将数据集中的所有图像编码为特征向量。 为此,我们首先需要将数据集中的所有图像一张一张地加载到内存中,并对其应用
encode_img函数。 首先,设置images文件夹的路径,如以下代码片段所示:
img_data = "./data/Flickr8k/dataset/images/"
完成后,我们使用先前创建的训练图像列表遍历文件夹中的所有图像,并对每个图像应用encode_img函数。 然后,将特征向量存储在以图像 ID 为键的字典中,如下所示:
train_encoded = {}
for ix, imgid in enumerate(train):
img_path = img_data + "/" + imgid + ".jpg"
train_encoded[imgid] = encode_img(img_path)
if ix%100 == 0:
print(".", end="")
我们类似地使用以下代码对测试数据集中的所有图像进行编码:
test_encoded = {}
for i, imgid in enumerate(test):
img_path = img_data + "/" + imgid + ".jpg"
test_encoded[imgid] = encode_img(img_path)
if i%100 == 0:
print(".", end="")
- 在接下来的几个步骤中,我们需要将加载的 GloVe 嵌入与项目中包含的单词列表进行匹配。 为此,我们当然必须找到任何给定单词的索引或在任何给定索引处找到该单词。 为方便起见,我们将在字幕数据集中找到的所有单词创建两个字典,将它们映射到索引和索引之间,如以下代码片段所示:
word_index_map = {}
index_word_map = {}
for i,word in enumerate(allwords):
word_index_map[word] = i+1
index_word_map[i+1] = word
我们还将在两个字典中分别使用"#START#"和"#STOP#"字创建两个附加的键值对,如下所示:
index_word_map[len(index_word_map)] = "#START#"
word_index_map["#START#"] = len(index_word_map)
index_word_map[len(index_word_map)] = "#STOP#"
word_index_map["#STOP#"] = len(index_word_map)
- 现在,将 GloVe 嵌入内容加载到项目中,如下所示:
f = open("./data/glove/glove.6B.50d.txt", encoding='utf8')
使用发现open,我们将嵌入内容读入字典,其中每个词都是键,如下所示:
embeddings = {}
for line in f:
words = line.split()
word_embeddings = np.array(words[1:], dtype='float')
embeddings[words[0]] = word_embeddings
读取完embeddings文件后,我们将其关闭以实现更好的内存管理,如下所示:
f.close()
- 现在,让我们在数据集中的标题中的所有单词与 GloVe 嵌入之间创建嵌入矩阵,如以下代码块所示:
embedding_matrix = np.zeros((len(word_index_map) + 1, 50))
for word, index in word_index_map.items():
embedding_vector = embeddings.get(word)
if embedding_vector is not None:
embedding_matrix[index] = embedding_vector
请注意,我们存储的最大嵌入数量为 50,这对于生成长而有意义的字符串是足够的。
- 接下来,我们将创建另一个模型,该模型将在从之前的步骤中获取特征向量后,专门用于为看不见的图像生成标题。 为此,我们将特征向量的形状作为输入来创建
Input层,如以下代码块所示:
in_img_feats = Input(shape=(2048,))
in_img_1 = Dropout(0.3)(in_img_feats)
in_img_2 = Dense(256, activation='relu')(in_img_1)
完成后,我们还需要以 LSTM 的形式在整个训练数据集中的标题中输入单词,以便给定任何单词,我们都能够预测接下来的 50 个单词。 我们使用以下代码进行操作:
in_caps = Input(shape=(max_caption_len,))
in_cap_1 = Embedding(input_dim=len(word_index_map) + 1, output_dim=50, mask_zero=True)(in_caps)
in_cap_2 = Dropout(0.3)(in_cap_1)
in_cap_3 = LSTM(256)(in_cap_2)
最后,我们需要添加一个decoder层,该层以 LSTM 的形式接受图像特征和单词,并在字幕生成过程中输出下一个可能的单词,如下所示:
decoder_1 = add([in_img_2, in_cap_3])
decoder_2 = Dense(256, activation='relu')(decoder_1)
outputs = Dense(len(word_index_map) + 1, activation='softmax')(decoder_2)
现在,通过运行以下代码,在适当添加输入和输出层之后,让我们对该模型进行总结:
model = Model(inputs=[in_img_feats, in_caps], outputs=outputs)
model.summary()
我们得到以下输出,描述了模型层:
接下来,让我们在训练模型之前设置其权重。
- 我们将在 GloVe 嵌入中的单词和数据集的标题中的可用单词之间插入我们先前创建的
embedding_matrix,如以下代码块所示:
model.layers[2].set_weights([embedding_matrix])
model.layers[2].trainable = False
这样,我们就可以编译模型了,如下所示:
model.compile(loss='categorical_crossentropy', optimizer='adam')
- 由于数据集很大,因此我们不想在训练时将所有图像同时加载到数据集中。 为了促进模型的内存有效训练,我们使用生成器函数,如下所示:
def data_generator(train_descs, train_encoded, word_index_map, max_caption_len, batch_size):
X1, X2, y = [], [], []
n = 0
while True:
for key, desc_list in train_descs.items():
n += 1
photo = train_encoded[key]
for desc in desc_list:
seq = [word_index_map[word] for word in desc.split() if word in word_index_map]
for i in range(1, len(seq)):
xi = seq[0:i]
yi = seq[i]
xi = pad_sequences([xi], maxlen=max_caption_len, value=0, padding='post')[0]
yi = to_categorical([yi], num_classes=len(word_index_map) + 1)[0]
X1.append(photo)
X2.append(xi)
y.append(yi)
if n==batch_size:
yield [[np.array(X1), np.array(X2)], np.array(y)]
X1, X2, y = [], [], []
n = 0
- 我们现在准备训练模型。 在执行此操作之前,我们必须设置模型的一些超参数,如以下代码片段所示:
batch_size = 3
steps = len(train_desc)//batch_size
设置超参数后,我们可以使用以下代码行开始训练:
generator = data_generator(train_desc, train_encoded, word_index_map, max_caption_len, batch_size)
model.fit_generator(generator, epochs=1, steps_per_epoch=steps, verbose=1)
model.save('./model_weights/model.h5')
测试
现在,在以下步骤中,我们将基于前面步骤中训练的模型创建用于预测字幕的功能,并在示例图像上测试字幕:
- 我们终于到了可以使用模型生成图像标题的阶段。 我们创建了一个函数,该函数可以吸收图像并使用
model.predict方法在每个步骤中提出一个单词,直到在预测中遇到#STOP#。 它在那里停止并输出生成的字幕,如下所示:
def predict_caption(img):
in_text = "#START#"
for i in range(max_caption_len):
sequence = [word_index_map[w] for w in in_text.split() if w in word_index_map]
sequence = pad_sequences([sequence], maxlen=max_caption_len, padding='post')
pred = model.predict([img, sequence])
pred = pred.argmax()
word = index_word_map[pred]
in_text += (' ' + word)
if word == "#STOP#":
break
caption = in_text.split()[1:-1]
return ' '.join(caption)
- 让我们在测试数据集中的某些图像上测试生成模型,如下所示:
img_name = list(test_encoded.keys())[np.random.randint(0, 1000)]
img = test_encoded[img_name].reshape((1, 2048))
im = plt.imread(img_data + img_name + '.jpg')
caption = predict_caption(img)
print(caption)
plt.imshow(im)
plt.axis('off')
plt.show()
假设我们将以下屏幕截图中显示的图像输入了算法:
对于前面的屏幕快照中显示的图像,我们获得了以下生成的标题:一只棕色的狗正穿过草丛。 虽然标题不是很准确,但完全遗漏了图片中的第二只动物,但它的确足以确定一条棕色的狗在草地上奔跑。
但是,我们训练有素的模型非常不准确,因此不适合用于生产或实验以外的用途。 您可能已经注意到,我们将训练中的周期数设置为 1,这是一个非常低的值。 这样做是为了使该程序的训练在合理的时间内完成,以供您阅读本书!
在下一节中,我们将研究如何将图像字幕生成模型部署为 API 并使用它来生成实时的摄像机供稿字幕。
创建一个简单的可单击部署的图像标题生成模型
虽然我们在上一节“测试”中开发的图像标题生成模型看起来不错,但不是很好。 因此,在本节中,我们将向您展示一种方法,以单击方式将可直接用于生产环境的模型作为 Docker 映像部署在 Red Hat OpenShift 上,并由 IBM 出色的机器学习专家创建。
将微服务用于在任何网站上执行的此类微小且专用的操作是一种非常普遍的做法,因此,我们将把此图像标题服务视为微服务。
我们将使用的图像是 IBM 开发的 MAX 图像字幕生成器模型。 它基于im2txt模型的代码,作为 《Show and Tell: Lessons learned from the 2015 MSCOCO Image Captioning Challenge》论文的可公开使用的 TensorFlow 实现托管在 GitHub 上。
在更大的 Microsoft COCO 数据集上训练了图像中使用的模型,该数据集包含超过 200,000 个带标签图像的实例,以及总共超过 300,000 个图像实例。 该数据集包含包含超过 150 万个不同对象的图像,并且是用于构建对象检测和图像标记模型的最大,最受欢迎的数据集之一。 但是,由于其巨大的尺寸,很难在低端设备上训练模型。 因此,我们将使用已经可用的 Docker 映像,而不是尝试在其上训练我们的模型。 但是,项目章节前面各节中描述的方法与 Docker 映像中的代码所使用的方法非常相似,并且在有足够的可用资源的情况下,您绝对可以尝试训练并提高模型的准确率。
您可以在以下链接中查看有关此 Docker 映像项目的所有详细信息。
您可以在此 Docker 映像的项目页面上了解其他可用的方法来部署此映像,但我们将向您展示在 Red Hat OpenShift 上的部署,从而使您只需单击几下即可快速测试模型。 。
让我们看看如何部署此映像,如下所示:
- 创建一个 Red Hat OpenShift 帐户。 为此,请将浏览器指向这里,然后单击“免费试用”。
- 选择尝试 RedHat OpenShift Online,如以下屏幕截图所示:
- 在下一个屏幕中,选择“注册 Openshift Online”。 然后,单击页面右上方的“注册”以找到“注册”页面。
- 填写所有必要的详细信息,然后提交表格。 系统将要求您进行电子邮件验证,完成后将带您进入订阅确认页面,该页面将要求您确认平台免费订阅的详细信息,如以下屏幕快照所示:
请注意,前面的订阅详细信息随时可能更改,并且可能反映订阅的其他值,区域或持续时间。
- 确认订阅后,您将需要等待几分钟才能配置系统资源。 设置完成后,您应该能够看到将带您进入管理控制台的按钮,如以下屏幕截图所示:
在上一个屏幕快照中显示的管理控制台的左侧,您可以找到各种菜单选项,并且在当前页面的中心,将提示您创建一个新项目。
- 单击“创建项目”,然后在出现的对话框中填写项目名称。 确保您创建的项目具有唯一的名称。 创建项目后,将为您提供一个仪表板,其中显示了对所有可用资源及其使用情况的监视。
在左侧菜单上,选择“开发人员”以切换到控制台的“开发人员”视图,如以下屏幕截图所示:
- 现在,您应该能够看到控制台的 Developer 视图以及更新的左侧菜单。 在这里,单击“拓扑”以获取以下部署选项:
- 在显示有部署选项的屏幕中单击“容器映像”,以调出用于容器映像部署的表单。
在此处,将图像名称填写为codait/max-image-caption-generator,然后单击“搜索”图标。 其余字段将自动获取,并且将显示与图像有关的信息,如以下屏幕截图所示:
- 在显示部署详细信息的下一个屏幕中,单击屏幕中央的“部署的映像”选项,如以下屏幕截图所示:
- 然后,向下滚动显示在屏幕右侧的信息面板,找到“路由”信息,该信息类似于以下屏幕截图:
单击此路由,将为您提供以下已成功部署的 API 的 Swagger UI:
您可以通过将图像发布到/model/predict路由来快速检查模型的工作情况。 随意使用 Swagger UI 可以很好地了解其表现。 您也可以使用/model/metadata路由找到模型元数据。
我们准备在项目中使用此 API。 让我们在接下来的部分中了解如何构建相机应用以及如何将此 API 集成到应用中。 我们首先使用相机插件构建应用。
了解相机插件
通过camera依赖项提供的相机插件,使我们可以自由访问设备的摄像机。 它为 Android 和 iOS 设备提供支持。 该插件是开源的,并托管在 GitHub 上,因此任何人都可以自由访问代码,修复错误并提出对当前版本的增强建议。
该插件可用于在小部件上显示实时摄像机预览,捕获图像并将其本地存储在设备上。 它也可以用来录制视频。 此外,它具有访问图像流的功能。
可以通过以下三个简单步骤将相机插件添加到任何应用:
- 安装包
- 添加用于持久存储和正确执行的方法
- 编程
现在让我们详细讨论每个步骤。
安装相机插件
要在应用中使用相机插件,我们需要在pubspec.yaml文件中添加camera作为依赖项。 可以按照以下步骤进行:
camera: 0.5.7+3
最后,运行flutter pub get将依赖项添加到应用。
添加用于持久存储和正确执行的方法
对于 iOS 设备,我们还需要指定一个空间来存储系统可以轻松访问的配置数据。 iOS 设备借助Info.plist文件来确定要显示的图标,应用支持的文档类型以及其他行为。 您需要在此步骤中修改ios/Runner/Info.plist中存在的Info.plist文件。
这可以通过添加以下文本来完成:
<key>NSCameraUsageDescription</key>
<string>Can I use the camera please?</string>
<key>NSMicrophoneUsageDescription</key>
<string>Can I use the mic please?</string>
对于 Android 设备,插件正常运行所需的最低软件开发套件(SDK)版本是 21。因此,请将最低 Android SDK 版本更改为 21(或更高版本), 存储在android/app/build.gradle文件中,如下所示:
minSdkVersion 21
安装依赖项并进行必要的更改之后,现在让我们开始编写应用代码。
编码
安装插件并进行必要的修改后,现在就可以使用它来访问相机,单击图片并录制视频。
涉及的最重要步骤如下:
- 通过运行以下代码导入插件:
import 'package:camera/camera.dart';
- 通过运行以下代码来检测可用的摄像机:
List<CameraDescription> cameras = await availableCameras();
- 初始化相机控件实例,如下所示:
CameraController controller = CameraController(cameras[0], ResolutionPreset.medium);
controller.initialize().then((_) {
if (!mounted) {
return;
}
setState(() {});
});
- 通过运行以下代码来处理控制器实例:
controller?.dispose();
现在,我们已经具备了相机插件的基本知识,让我们为应用构建实时相机预览。
创建相机应用
现在,我们将开始构建移动应用,以为指向相机的对象生成标题。 它包括一个用于捕获图像的相机预览和一个用于显示模型返回的字幕的文本视图。
该应用可以大致分为两部分,如下所示:
- 建立相机预览
- 集成模型来获取标题
在以下部分中,我们将讨论构建基本的相机预览。
建立相机预览
现在,我们将为应用构建摄像机预览。 我们首先创建一个新文件generate_live_caption.dart和一个GenerateLiveCaption有状态小部件。
让我们看一下创建实时摄像机预览的以下步骤:
- 要添加实时摄像机预览,我们将使用
camera插件。 首先,将依存关系添加到pubspec.yaml文件中,如下所示:
camera: ^0.5.7
接下来,我们需要通过运行flutter pub get将依赖项添加到项目中。
-
现在,我们创建一个新文件
generate_live_captions.dart,其中包含GenerateLiveCaptions有状态的小部件。 进一步步骤中描述的所有代码将包含在_GenerateLiveCaptionState类中。 -
导入
camera库。 我们将其导入generate_live_captions.dart,如下所示:
import 'package:camera/camera.dart';
- 现在,我们需要检测设备上所有可用的摄像机。 为其定义
detectCameras()函数,如下所示:
Future<void> detectCameras() async{
cameras = await availableCameras();
}
cameras是包含所有可用摄像机的全局列表,并在GenerateLiveCaptionState中声明,如下所示:
List<CameraDescription> cameras;
- 现在,我们使用
initializeController()方法创建CameraController的实例,如下所示:
void initializeController() {
controller = CameraController(cameras[0], ResolutionPreset.medium);
controller.initialize().then((_) {
if (!mounted) {
return;
}
setState(() {});
});
}
在应用中,我们将使用设备的后置摄像头,因此我们使用camera[0]创建CameraController实例,并使用ResolutionPreset.medium将分辨率指定为中等。 接下来,我们使用controller.initialize()初始化控制器。
- 为了在应用的屏幕上显示摄像机源,我们定义了
buildCameraPreview()方法,如下所示:
Widget _buildCameraPreview() {
var size = MediaQuery.of(context).size.width;
return Container(
child: Column(
children: <Widget>[
Container(
width: size,
height: size,
child: CameraPreview(controller),
),
]
)
);
}
在前面的方法中,我们使用MediaQuery.of(context).size.width获取容器的宽度并将其存储在size变量中。 接下来,我们创建一列小部件,其中第一个元素是Container。 Container的子项只是CameraPreview,用于在应用的屏幕上显示摄像机的信息。
- 现在,我们覆盖
initState,以便在初始化GenerateLiveCaption后立即检测到所有摄像机,如下所示:
@override
void initState() {
super.initState();
detectCameras().then((_){
initializeController();
});
}
在前面的代码片段中,我们仅调用detectCameras()首先检测所有可用的摄像机,然后调用initializeController()用后置摄像机初始化CameraController。
- 要从相机供稿生成字幕,我们将从相机供稿中拍摄照片并将其存储在本地设备中。 这些单击的图片将稍后从图像文件中检索以生成标题。 因此,我们需要一种读取和写入文件的机制。 我们通过在
pubspec.yaml文件中添加以下依赖项来使用path_provider插件:
path_provider: ^1.4.5
接下来,我们通过在终端中运行flutter pub get来安装包。
- 要在应用中使用
path_provider插件,我们需要通过在文件顶部添加import语句将其导入generate_live_caption.dart中,如下所示:
import 'package:path_provider/path_provider.dart';
- 要将图像文件保存到磁盘,我们还需要导入
dart:io库,如下所示:
import 'dart:io';
- 现在,让我们定义一种方法
captureImages(),以从相机源中捕获图像并将其存储在设备中。 这些存储的图像文件将在以后用于生成字幕。 该方法定义如下:
capturePictures() async {
String timestamp = DateTime.now().millisecondsSinceEpoch.toString();
final Directory extDir = await getApplicationDocumentsDirectory();
final String dirPath = '${extDir.path}/Pictures/generate_caption_images';
await Directory(dirPath).create(recursive: true);
final String filePath = '$dirPath/${timestamp}.jpg';
controller.takePicture(filePath).then((_){
File imgFile = File(filePath);
});
}
在前面的代码片段中,我们首先使用DateTime.now().millisecondsSinceEpoch()找出当前时间(以毫秒为单位),然后将其转换为字符串并将其存储在变量timestamp中。 时间戳将用于为我们将进一步存储的图像文件提供唯一的名称。 接下来,我们使用getApplicationDocumentsDirectory()获取可用于存储图像的目录的路径,并将其存储在Directory类型的extDir中。 现在,我们通过在外部目录后附加'/Pictures/generate_caption_images'来创建适当的目录路径。 然后,我们通过将目录路径与当前时间戳组合并为其指定.jpg格式来创建最终的filePath。 由于时间戳始终具有不同的值,因此所有单击的图像的filePath将始终是唯一的。 最后,我们使用当前的相机控制器实例调用takePicture()并传入filePath来捕获图像。 我们存储在imgFile中创建的图像文件,稍后将用于生成适当的字幕。
- 如前所述,为了从实时摄像机的提要中生成字幕,我们会定期捕获图像。 为了使它起作用,我们修改
initializeController()并添加一个计时器,如下所示:
void initializeController() {
controller = CameraController(cameras[0], ResolutionPreset.medium);
controller.initialize().then((_) {
if (!mounted) {
return;
}
setState(() {});
const interval = const Duration(seconds:5);
new Timer.periodic(interval, (Timer t) => capturePictures());
});
}
在initializeController()内部,一旦正确初始化并安装了摄像机控制器,我们将使用Duration()类创建 5 秒的持续时间,并将其存储在间隔中。 现在,我们使用Timer.periodic创建一个定期计时器,并为其设置 5 秒的间隔。 此处指定的回调为capturePictures()。 将在指定间隔内重复调用它。
至此,我们创建了一个实时摄像机供稿,该供稿显示在屏幕上,并且能够以 5 秒的间隔捕获图像。 在下一部分中,我们将集成模型以为所有捕获的图像生成标题。
从相机源生成图像字幕
现在,我们对图像标题生成器有了一个清晰的想法,并有了一个带有摄像头提要的应用,我们准备为摄像头提要生成图像的标题。 要遵循的逻辑非常简单。 图像是在特定时间间隔从实时摄像机的提要中捕获的,并存储在设备的本地存储中。 接下来,检索存储的图片,并为托管模型创建HTTP POST请求,传入检索的图像以获取生成的字幕,解析响应并将其显示在屏幕上。
现在让我们看一下详细步骤,如下所示:
- 我们首先将
http依赖项添加到pubspec.yaml文件,以发出http请求,如下所示:
http: ^0.12.0
使用flutter pub get将依赖项安装到项目。
- 要在应用中使用
http包,我们需要将其导入generate_live_caption.dart中,如下所示:
import 'package:http/http.dart' as http;
- 现在,我们定义一个方法
fetchResponse(),它使用一个图像文件并使用该图像为托管模型创建一个帖子,如下所示:
Future<Map<String, dynamic>> fetchResponse(File image) async {
final mimeTypeData =
lookupMimeType(image.path, headerBytes: [0xFF, 0xD8]).split('/');
final imageUploadRequest = http.MultipartRequest(
'POST',
Uri.parse(
"http://max-image-caption-generator-mytest865.apps.us-east-2.starter.openshift-online.com/model/predict"));
final file = await http.MultipartFile.fromPath('image', image.path,
contentType: MediaType(mimeTypeData[0], mimeTypeData[1]));
imageUploadRequest.fields['ext'] = mimeTypeData[1];
imageUploadRequest.files.add(file);
try {
final streamedResponse = await imageUploadRequest.send();
final response = await http.Response.fromStream(streamedResponse);
final Map<String, dynamic> responseData = json.decode(response.body);
parseResponse(responseData);
return responseData;
} catch (e) {
print(e);
return null;
}
}
在上述方法中,我们首先通过查看文件的头字节来找到所选文件的 mime 类型。 然后,我们按照托管 API 的要求初始化一个多部分请求。 我们将传递给函数的文件附加为image POST 参数。 由于image_picker存在一些错误,因此错误地将图像扩展名与文件名(例如filenamejpeg)混合在一起,因此我们在请求正文中明确传递了图像扩展名,这会在服务器端管理或验证文件扩展名时产生问题。 响应采用 JSON 格式,因此,我们需要使用json.decode()对其进行解码,并使用res.body传入响应的主体。 现在,我们通过调用下一步定义的parseResponse()来解析响应。 此外,我们使用catchError()检测并打印执行POST请求时可能发生的任何错误。
- 成功执行
POST请求并从模型中获得带有传递的图像的标题的响应之后,我们在parseResponse()方法内部解析响应,如下所示:
void parseResponse(var response) {
String resString = "";
var predictions = response['predictions'];
for(var prediction in predictions) {
var caption = prediction['caption'];
var probability = prediction['probability'];
resString = resString + '${caption}: ${probability}\n\n';
}
setState(() {
resultText = resString;
});
}
在上述方法中,我们首先存储response['predictions']中存在的所有预测的列表,并将其存储在prediction变量中。 现在,我们使用prediction变量遍历for each循环内的每个预测。 对于每个预测,我们分别取出prediction['caption']和prediction['probability']中存储的标题和概率。 我们将它们附加到resString字符串变量,该变量将包含所有预测的字幕以及概率。 最后,我们将resultText的状态设置为resString中存储的值。 resultText是此处的全局字符串变量,将在接下来的步骤中使用它来显示预测的字幕。
- 现在,我们修改
capturePictures(),以便每次捕获新图像时都会发出 HTTP 发布请求,如下所示:
capturePictures() async {
. . . . .
controller.takePicture(filePath).then((_){
File imgFile = File(filePath);
fetchResponse(imgFile);
});
}
在前面的代码片段中,我们向fetchResponse()添加了一个调用,并传入了图像文件。
- 现在,让我们修改
buildCameraPreview()以显示所有预测,如下所示:
Widget buildCameraPreview() {
. . . . .
return Container(
child: Column(
children: <Widget>[
Container(
. . . . .
child: CameraPreview(controller),
),
Text(resultText),
]
)
);
}
在前面的代码片段中,我们简单地将Text与result.Text相加。 result.Text是一个全局字符串变量,它将包含“步骤 5”中所述的所有预测,并声明如下:
String resultText = "Fetching Response..";
- 最后,我们重写
build()方法以为应用创建最终的脚手架,如下所示:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Generate Image Caption'),),
body: (controller.value.isInitialized)?buildCameraPreview():new Container(),
);
}
在前面的代码片段中,我们返回了一个标题为Generate Image Caption的appBar支架。 主体最初设置为空容器。 初始化摄像机控制器后,将更新主体以显示摄像机供稿以及预测的字幕。
- 最后,我们按以下方式处置摄像头控制器:
@override
void dispose() {
controller?.dispose();
super.dispose();
}
现在,我们已经成功创建了一种在屏幕上显示实时摄像机供稿的机制。 实时摄像头的提要以 5 秒的间隔被捕获,并作为输入发送到模型。 然后,所有捕获图像的预测字幕将显示在屏幕上。
在下一节中,我们现在创建最终的材质应用以将所有内容整合在一起。
创建材质应用
在使所有段正常工作之后,让我们创建最终的材质应用。 在main.dart文件中,我们创建StatelessWidget并覆盖build()方法,如下所示:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: GenerateLiveCaption()
);
}
}
最后,我们执行以下代码:
void main() => runApp(MyApp());
您应该能够拥有一个应用屏幕,如以下屏幕截图所示:
请注意图像中显示的标题,如下所示:
- 放在桌子上的便携式计算机。
- 放在桌子上的一台打开的便携式计算机。
- 放在一张木桌上的一台打开的便携式计算机。
这些标题的描述非常准确。 但是,由于训练数据集中相关图片的不可用,它们有时可能表现不佳。
总结
在本章中,我们了解了如何创建一个应用,该应用使用深层的 CNN 和 LSTM 为摄像机的提要实时生成字幕。 我们还看到了如何快速将以 Docker 映像形式提供的某些机器学习/深度学习模型部署到 Red Hat OpenShift,并以可调用 API 的形式轻松获取它们。 从应用开发人员的角度来看,这是至关重要的,因为当与一组机器学习开发人员一起工作时,他们通常会为您提供要使用的模型的 Docker 映像,这样您就无需在其中执行任何代码或配置。 系统。 可以将这种应用用于多种用途,例如为盲人创建辅助技术,生成当时发生的事件的成绩单,或者(例如)为孩子提供现场指导,以帮助他们识别环境中的物体。 我们介绍了如何应用 Flutter 相机插件并在框架上进行深度学习。
在下一章中,我们将研究如何开发用于执行应用安全性的深度学习模型。
六、构建人工智能认证系统
认证是任何应用中最突出的功能之一,无论它是本机移动软件还是网站,并且自从保护数据的需求以及与机密有关的隐私需求开始以来,认证一直是一个活跃的领域。 在互联网上共享的数据。 在本章中,我们将从基于 Firebase 的简单登录到应用开始,然后逐步改进以包括基于人工智能(AI)的认证置信度指标和 Google 的 ReCaptcha。 所有这些认证方法均以深度学习为核心,并提供了一种在移动应用中实现安全性的最新方法。
在本章中,我们将介绍以下主题:
- 一个简单的登录应用
- 添加 Firebase 认证
- 了解用于认证的异常检测
- 用于认证用户的自定义模型
- 实现 ReCaptcha 来避免垃圾邮件
- 在 Flutter 中部署模型
技术要求
对于移动应用,需要具有 Flutter 的 Visual Studio Code 和 Dart 插件以及 Firebase Console
一个简单的登录应用
我们将首先创建一个简单的认证应用,该应用使用 Firebase 认证对用户进行认证,然后再允许他们进入主屏幕。 该应用将允许用户输入其电子邮件和密码来创建一个帐户,然后使他们随后可以使用此电子邮件和密码登录。
以下屏幕快照显示了应用的完整流程:
该应用的小部件树如下:
现在让我们详细讨论每个小部件的实现。
创建 UI
让我们从创建应用的登录屏幕开始。 用户界面(UI)将包含两个TextFormField来获取用户的电子邮件 ID 和密码,RaisedButton进行注册/登录,以及FlatButton进行注册和登录操作之间的切换。
以下屏幕快照标记了将用于应用的第一个屏幕的小部件:
现在让我们创建应用的 UI,如下所示:
- 我们首先创建一个名为
signup_signin_screen.dart的新 dart 文件。 该文件包含一个有状态的小部件–SignupSigninScreen。 - 第一个屏幕中最上面的窗口小部件是
TextField,用于获取用户的邮件 ID。_createUserMailInput()方法可帮助我们构建窗口小部件:
Widget _createUserMailInput() {
return Padding(
padding: const EdgeInsets.fromLTRB(0.0, 100.0, 0.0, 0.0),
child: new TextFormField(
maxLines: 1,
keyboardType: TextInputType.emailAddress,
autofocus: false,
decoration: new InputDecoration(
hintText: 'Email',
icon: new Icon(
Icons.mail,
color: Colors.grey,
)),
validator: (value) => value.isEmpty ? 'Email can\'t be empty' : null,
onSaved: (value) => _usermail = value.trim(),
),
);
}
首先,我们使用EdgeInsets.fromLTRB()为小部件提供了填充。 这有助于我们在四个基本方向的每个方向(即左,上,右和下)上创建具有不同值的偏移量。 接下来,我们使用maxLines(输入的最大行数)创建了TextFormField,其值为1作为子级,它接收用户的电子邮件地址。 另外,根据输入类型TextInputType.emailAddress,我们指定了将在属性keyboardType中使用的键盘类型。 然后,将autoFocus设置为false。 然后,我们在装饰属性中使用InputDecoration提供hintText "Email"和图标Icons.mail。 为了确保用户在没有输入电子邮件地址或密码的情况下不要尝试登录,我们添加了一个验证器。 当尝试使用空字段登录时,将显示警告“电子邮件不能为空”。 最后,我们通过使用trim()删除所有尾随空格来修剪输入的值,然后将输入的值存储在_usermail字符串变量中。
- 与“步骤 2”中的
TextField相似,我们定义了下一个方法_createPasswordInput(),以创建用于输入密码的TextFormField():
Widget _createPasswordInput() {
return Padding(
padding: const EdgeInsets.fromLTRB(0.0, 15.0, 0.0, 0.0),
child: new TextFormField(
maxLines: 1,
obscureText: true,
autofocus: false,
decoration: new InputDecoration(
hintText: 'Password',
icon: new Icon(
Icons.lock,
color: Colors.grey,
)),
validator: (value) => value.isEmpty ? 'Password can\'t be empty' : null,
onSaved: (value) => _userpassword = value.trim(),
),
);
}
我们首先使用EdgeInsets.fromLTRB()在所有四个基本方向上提供填充,以在顶部提供15.0的偏移量。 接下来,我们创建一个TextFormField,其中maxLines为1,并将obscureText设置为true,将autofocus设置为false。 obscureText用于隐藏正在键入的文本。 我们使用InputDecoration提供hintText密码和一个灰色图标Icons.lock。 为确保文本字段不为空,使用了一个验证器,当传递空值时,该警告器会发出警告Password can't be empty,即用户尝试在不输入密码的情况下登录/注册。 最后,trim()用于删除所有尾随空格,并将密码存储在_userpassword字符串变量中。
- 接下来,我们在
_SignupSigninScreenState外部声明FormMode枚举,该枚举在两种模式SIGNIN和SIGNUP之间运行,如以下代码片段所示:
enum FormMode { SIGNIN, SIGNUP }
我们将对该按钮使用此枚举,该按钮将使用户既可以登录又可以注册。 这将帮助我们轻松地在两种模式之间切换。 枚举是一组用于表示常量值的标识符。
使用enum关键字声明枚举类型。 在enum内部声明的每个标识符都代表一个整数值; 例如,第一标识符具有值0,第二标识符具有值1。 默认情况下,第一个标识符的值为0。
- 让我们定义一个
_createSigninButton()方法,该方法返回按钮小部件以使用户注册并登录:
Widget _createSigninButton() {
return new Padding(
padding: EdgeInsets.fromLTRB(0.0, 45.0, 0.0, 0.0),
child: SizedBox(
height: 40.0,
child: new RaisedButton(
elevation: 5.0,
shape: new RoundedRectangleBorder(borderRadius: new BorderRadius.circular(30.0)),
color: Colors.blue,
child: _formMode == FormMode.SIGNIN
? new Text('SignIn',
style: new TextStyle(fontSize: 20.0, color: Colors.white))
: new Text('Create account',
style: new TextStyle(fontSize: 20.0, color: Colors.white)),
onPressed: _signinSignup,
),
));
}
我们从Padding开始,将45.0的按钮offset置于顶部,然后将SizedBox和40.0的height作为子项,并将RaisedButton作为其子项。 使用RoundedRectangleBorder()为凸起的按钮赋予圆角矩形形状,其边框半径为30.0,颜色为blue。 作为子项添加的按钮的文本取决于_formMode的当前值。 如果_formMode的值(FormMode枚举的一个实例)为FormMode.SIGNIN,则按钮显示SignIn,否则创建帐户。 按下按钮时将调用_signinSignup方法,该方法将在后面的部分中介绍。
- 现在,我们将第四个按钮添加到屏幕上,以使用户在
SIGNIN和SIGNUP表单模式之间切换。 我们定义返回FlatButton的_createSigninSwitchButton()方法,如下所示:
Widget _createSigninSwitchButton() {
return new FlatButton(
child: _formMode == FormMode.SIGNIN
? new Text('Create an account',
style: new TextStyle(fontSize: 18.0, fontWeight: FontWeight.w300))
: new Text('Have an account? Sign in',
style:
new TextStyle(fontSize: 18.0, fontWeight: FontWeight.w300)),
onPressed: _formMode == FormMode.SIGNIN
? _switchFormToSignUp
: _switchFormToSignin,
);
}
如果_formMode的当前值为SIGNIN并按下按钮,则应更改为SIGNUP并显示Create an account。 否则,如果_formMode将SIGNUP作为其当前值,并且按下按钮,则该值应切换为由文本Have an account? Sign in表示的SIGNIN。 使用三元运算符创建RaisedButton的Text子级时,添加了在文本之间切换的逻辑。 onPressed属性使用非常相似的逻辑,该逻辑再次检查_formMode的值以在模式之间切换并使用_switchFormToSignUp和_switchFormToSignin方法更新_formMode的值。 我们将在“步骤 7”和 8 中定义_switchFormToSignUp和_switchFormToSignin方法。
- 现在,我们定义
_switchFormToSignUp()如下:
void _switchFormToSignUp() {
_formKey.currentState.reset();
setState(() {
_formMode = FormMode.SIGNUP;
});
}
此方法重置_formMode的值并将其更新为FormMode.SIGNUP。 更改setState()内部的值有助于通知框架该对象的内部状态已更改,并且 UI 可能需要更新。
- 我们以与
_switchFormToSignUp()非常相似的方式定义_switchFormToSignin():
void _switchFormToSignin() {
_formKey.currentState.reset();
setState(() {
_formMode = FormMode.SIGNIN;
});
}
此方法重置_formMode的值并将其更新为FormMode.SIGNIN。 更改setState()内部的值有助于通知框架该对象的内部状态已更改,并且 UI 可能需要更新。
- 现在,让我们将所有屏幕小部件
Email TextField,Password TextFied,SignIn Button和FlatButton切换为在单个容器中进行注册和登录。 为此,我们定义了一种方法createBody(),如下所示:
Widget _createBody(){
return new Container(
padding: EdgeInsets.all(16.0),
child: new Form(
key: _formKey,
child: new ListView(
shrinkWrap: true,
children: <Widget>[
_createUserMailInput(),
_createPasswordInput(),
_createSigninButton(),
_createSigninSwitchButton(),
_createErrorMessage(),
],
),
)
);
}
此方法返回一个以Form作为子元素的新Container并为其填充16.0。 表单使用_formKey作为其键,并添加ListView作为其子级。 ListView的元素是我们在前述方法中创建的用于添加TextFormFields和Buttons的小部件。 shrinkWrap设置为true,以确保ListView仅占用必要的空间,并且不会尝试扩展和填充整个屏幕
Form类用于将多个FormFields一起分组和验证。 在这里,我们使用Form将两个TextFormFields,一个RaisedButton和一个FlatButton包装在一起。
- 这里要注意的一件事是,由于进行认证,因此用户最终将成为网络操作,因此可能需要一些时间来发出网络请求。 在此处添加进度条可防止在进行网络操作时 UI 的死锁。 我们声明
boolean标志_loading,当网络操作开始时将其设置为true。 现在,我们定义一种_createCircularProgress()方法,如下所示:
Widget _createCircularProgress(){
if (_loading) {
return Center(child: CircularProgressIndicator());
} return Container(height: 0.0, width: 0.0,);
}
仅当_loading为true并且正在进行网络操作时,该方法才返回CircularProgressIndicator()。
- 最后,让我们在
build()方法内添加所有小部件:
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Firebase Authentication'),
),
body: Stack(
children: <Widget>[
_createBody(),
_createCircularProgress(),
],
));
}
从build()内部,添加包含应用标题的AppBar变量后,我们返回一个支架。 支架的主体包含一个带有子项的栈,这些子项是_createBody()和_createCircularProgress() 函数调用返回的小部件。
现在,我们已经准备好应用的主要 UI 结构。
可以在这个页面中找到SignupSigninScreen的完整代码。
在下一部分中,我们将介绍将 Firebase 认证添加到应用中涉及的步骤。
添加 Firebase 认证
如前所述,在“简单登录应用”部分中,我们将使用用户的电子邮件和密码通过 Firebase 集成认证。
要在 Firebase 控制台上创建和配置 Firebase 项目,请参考“附录”。
以下步骤详细讨论了如何在 Firebase Console 上设置项目:
- 我们首先在 Firebase 控制台上选择项目:
- 接下来,我们将在
Develop菜单中单击Authentication选项:
这将带我们进入认证屏幕。
- 迁移到登录标签并启用登录提供者下的“电子邮件/密码”选项:
这是设置 Firebase 控制台所需的全部。
接下来,我们将 Firebase 集成到代码中。 这样做如下:
- 迁移到 Flutter SDK 中的项目,然后将
firebase-auth添加到应用级别build.gradle文件中:
implementation 'com.google.firebase:firebase-auth:18.1.0'
- 为了使
FirebaseAuthentication在应用中正常工作,我们将在此处使用firebase_auth插件。 在pubspec.yaml文件的依赖项中添加插件依赖项:
firebase_auth: 0.14.0+4
确保运行flutter pub get以安装依赖项。
现在,让我们编写一些代码以在应用内部提供 Firebase 认证功能。
创建auth.dart
现在,我们将创建一个 Dart 文件auth.dart。 该文件将作为访问firebase_auth插件提供的认证方法的集中点:
- 首先,导入
firebase_auth插件:
import 'package:firebase_auth/firebase_auth.dart';
- 现在,创建一个抽象类
BaseAuth,该类列出了所有认证方法,并充当 UI 组件和认证方法之间的中间层:
abstract class BaseAuth {
Future<String> signIn(String email, String password);
Future<String> signUp(String email, String password);
Future<String> getCurrentUser();
Future<void> signOut();
}
顾名思义,这些方法将使用认证的四个主要函数:
signIn():使用电子邮件和密码登录已经存在的用户signUp():使用电子邮件和密码为新用户创建帐户getCurrentUser():获取当前登录的用户signOut():注销已登录的用户
这里要注意的重要一件事是,由于这是网络操作,因此所有方法都异步操作,并在执行完成后返回Future值。
- 创建一个实现
BaseAuth的Auth类:
class Auth implements BaseAuth {
//. . . . .
}
在接下来的步骤中,我们将定义BaseAuth中声明的所有方法。
- 创建
FirebaseAuth的实例:
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
signIn()方法实现如下:
Future<String> signIn(String email, String password) async {
AuthResult result = await _firebaseAuth.signInWithEmailAndPassword(email: email, password: password);
FirebaseUser user = result.user;
return user.uid;
}
此方法接收用户的电子邮件和密码,然后调用signInWithEmailAndPassword(),并传递电子邮件和密码以登录已经存在的用户。 登录操作完成后,将返回AuthResult实例。 我们将其存储在result中,还使用result.user,它返回FirebaseUser.。它可用于获取与用户有关的信息,例如他们的uid,phoneNumber和photoUrl。 在这里,我们返回user.uid,它是每个现有用户的唯一标识。 如前所述,由于这是网络操作,因此它异步运行,并在执行完成后返回Future。
- 接下来,我们将定义
signUp()方法以添加新用户:
Future<String> signUp(String email, String password) async {
AuthResult result = await _firebaseAuth.createUserWithEmailAndPassword(email: email, password: password);
FirebaseUser user = result.user;
return user.uid;
}
前面的方法接收在注册过程中使用的电子邮件和密码,并将其值传递给createUserWithEmailAndPassword。 类似于上一步中定义的对象,此调用还返回AuthResult对象,该对象还用于提取FirebaseUser。 最后,signUp方法返回新创建的用户的uid。
- 现在,我们将定义
getCurrentUser():
Future<String> getCurrentUser() async {
FirebaseUser user = await _firebaseAuth.currentUser();
return user.uid;
}
在先前定义的函数中,我们使用_firebaseAuth.currentUser()提取当前登录用户的信息。 此方法返回包装在FirebaseUser对象中的完整信息。 我们将其存储在user变量中。 最后,我们使用user.uid返回用户的uid。
- 接下来,我们执行
signOut():
Future<void> signOut() async {
return _firebaseAuth.signOut();
}
此函数仅在当前FirebaseAuth实例上调用signOut()并注销已登录的用户。
至此,我们已经完成了用于实现 Firebase 认证的所有基本编码。
可以在这个页面中查看auth.dart中的整个代码。
现在让我们看看如何在应用内部使认证生效。
在SignupSigninScreen中添加认证
在本节中,我们将在SignupSigninScreen中添加 Firebase 认证。
我们在signup_signin_screen.dart文件中定义了_signinSignup()方法。 当按下登录按钮时,将调用该方法。 该方法的主体如下所示:
void _signinSignup() async {
setState(() {
_loading = true;
});
String userId = "";
if (_formMode == FormMode.SIGNIN) {
userId = await widget.auth.signIn(_usermail, _userpassword);
} else {
userId = await widget.auth.signUp(_usermail, _userpassword);
}
setState(() {
_loading = false;
});
if (userId.length > 0 && userId != null && _formMode == FormMode.SIGNIN) {
widget.onSignedIn();
}
}
在上述方法中,我们首先将_loading的值设置为true,以便进度条显示在屏幕上,直到登录过程完成。 接下来,我们创建一个userId字符串,一旦登录/登录操作完成,该字符串将存储userId的值。 现在,我们检查_formMode的当前值。 如果等于FormMode.SIGNIN,则用户希望登录到现有帐户。 因此,我们使用传递到SignupSigninScreen构造器中的实例来调用Auth类内部定义的signIn()方法。
这将在后面的部分中详细讨论。 否则,如果_formMode的值等于FormMode.SIGNUP,则将调用Auth类的signUp()方法,并传递用户的邮件和密码以创建新帐户。 一旦成功完成登录/注册,userId变量将用于存储用户的 ID。 整个过程完成后,将_loading设置为false,以从屏幕上删除循环进度指示器。 另外,如果在用户登录到现有帐户时userId具有有效值,则将调用onSignedIn(),这会将用户定向到应用的主屏幕。
此方法也传递给SignupSigninScreen的构造器,并将在后面的部分中进行讨论。 最后,我们将整个主体包裹在try-catch块中,以便在登录过程中发生的任何异常都可以捕获而不会导致应用崩溃,并可以在屏幕上显示。
创建主屏幕
我们还需要确定认证状态,即用户在启动应用时是否已登录,如果已经登录,则将其定向到主屏幕。如果尚未登录,则应显示SignInSignupScreen 首先,在完成该过程之后,将启动主屏幕。 为了实现这一点,我们在新的 dart 文件main_screen.dart中创建一个有状态的小部件MainScreen,然后执行以下步骤:
- 我们将从定义枚举
AuthStatus开始,该枚举表示用户的当前认证状态,可以登录或不登录:
enum AuthStatus {
NOT_SIGNED_IN,
SIGNED_IN,
}
- 现在,我们创建
enum类型的变量来存储当前认证状态,其初始值设置为NOT_SIGNED_IN:
AuthStatus authStatus = AuthStatus.NOT_SIGNED_IN;
- 初始化小部件后,我们将通过覆盖
initState()方法来确定用户是否已登录:
@override
void initState() {
super.initState();
widget.auth.getCurrentUser().then((user) {
setState(() {
if (user != null) {
_userId = user;
}
authStatus =
user == null ? AuthStatus.NOT_SIGNED_IN : AuthStatus.SIGNED_IN;
});
});
}
使用在构造器中传递的类的实例调用Auth类的getCurrentUser()。 如果该方法返回的值不为null,则意味着用户已经登录。因此,_userId字符串变量的值设置为返回的值。 另外,将authStatus设置为AuthStatus.SIGNED_IN.,否则,如果返回的值为null,则意味着没有用户登录,因此authStatus的值设置为AuthStatus.NOT_SIGNED_IN。
- 现在,我们将定义另外两个方法
onSignIn()和onSignOut(),以确保将认证状态正确存储在变量中,并相应地更新用户界面:
void _onSignedIn() {
widget.auth.getCurrentUser().then((user){
setState(() {
_userId = user;
});
});
setState(() {
authStatus = AuthStatus.SIGNED_IN;
});
}
void _onSignedOut() {
setState(() {
authStatus = AuthStatus.NOT_SIGNED_IN;
_userId = "";
});
}
_onSignedIn()方法检查用户是否已经登录,并将authStatus设置为AuthStatus.SIGNED_IN.。 _onSignedOut()方法检查用户是否已注销,并将authStatus设置为AuthStatus.SIGNED_OUT。
- 最后,我们重写
build方法将用户定向到正确的屏幕:
@override
Widget build(BuildContext context) {
if(authStatus == AuthStatus.SIGNED_OUT) {
return new SignupSigninScreen(
auth: widget.auth,
onSignedIn: _onSignedIn,
);
} else {
return new HomeScreen(
userId: _userId,
auth: widget.auth,
onSignedOut: _onSignedOut,
);
}
}
如果authStatus为AuthStatus.SIGNED_OUT,则返回SignupSigninScreen,并传递auth实例和_onSignedIn()方法。 否则,将直接返回HomeScreen,并传递已登录用户的userId,Auth实例类和_onSignedOut()方法。
在下一部分中,我们将为应用添加一个非常简单的主屏幕。
创建主屏幕
由于我们对认证部分更感兴趣,因此主屏幕(即成功登录后指向用户的屏幕)应该非常简单。 它仅包含一些文本和一个注销选项。 正如我们对所有先前的屏幕和小部件所做的一样,我们首先创建一个home_screen.dart文件和一个有状态的HomeScreen小部件。
主屏幕将显示如下:
此处的完整代码位于重写的build()方法内部:
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Firebase Authentication'),
actions: <Widget>[
new FlatButton(
child: new Text('Logout',
style: new TextStyle(fontSize: 16.0, color: Colors.white)),
onPressed: _signOut
)
],
),
body: Center(child: new Text('Hello User',
style: new TextStyle(fontSize: 32.0))
),
);
}
我们在此处返回Scaffold,其中包含标题为Text Firebase Authentication的AppBar和actions属性的小部件列表。 actions用于在应用标题旁边添加小部件列表到应用栏中。 在这里,它仅包含FlatButton,Logout,在按下时将调用_signOut。
_signOut()方法显示如下:
_signOut() async {
try {
await widget.auth.signOut();
widget.onSignedOut();
} catch (e) {
print(e);
}
}
该方法主要是调用Auth类中定义的signOut()方法,以将用户从应用中注销。 回忆传入HomeScreen的MainScreen的_onSignedOut()方法。 当用户退出时,该方法在此处用作widget.onSignedOut()来将authStatus更改为SIGNED_OUT。 同样,它包装在try-catch块中,以捕获并打印此处可能发生的任何异常。
至此,应用的主要组件已经准备就绪,现在让我们创建最终的材质应用。
创建main.dart
在main.dart内部,我们创建Stateless Widget,App,并覆盖build()方法,如下所示:
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Firebase Authentication',
debugShowCheckedModeBanner: false,
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MainScreen(auth: new Auth()));
}
该方法从主屏幕返回MaterialApp,以提供标题,主题。
了解用于认证的异常检测
异常检测是机器学习的一个备受关注的分支。 该术语含义简单。 基本上,它是用于检测异常的方法的集合。 想象一袋苹果。 识别并挑选坏苹果将是异常检测的行为。
异常检测以几种方式执行:
- 通过使用列的最小最大范围来识别数据集中与其余样本非常不同的数据样本
- 通过将数据绘制为线形图并识别图中的突然尖峰
- 通过围绕高斯曲线绘制数据并将最末端的点标记为离群值(异常)
一些常用的方法是支持向量机,贝叶斯网络和 K 最近邻。 在本节中,我们将重点介绍与安全性相关的异常检测。
假设您通常在家中登录应用上的帐户。 如果您突然从数千英里外的位置登录帐户,或者在另一种情况下,您以前从未使用过公共计算机登录帐户,那将是非常可疑的,但是突然有一天您这样做。 另一个可疑的情况可能是您尝试 10-20 次密码,每次在成功成功登录之前每次都输入错误密码。 当您的帐户遭到盗用时,所有这些情况都是可能的行为。 因此,重要的是要合并一个能够确定您的常规行为并对异常行为进行分类的系统。 换句话说,即使黑客使用了正确的密码,企图破坏您的帐户的尝试也应标记为异常。
这带给我们一个有趣的观点,即确定用户的常规行为。 我们如何做到这一点? 什么是正常行为? 它是针对每个用户的还是一个通用概念? 问题的答案是它是非常特定于用户的。 但是,行为的某些方面对于所有用户而言都可以相同。 一个应用可能会在多个屏幕上启动登录。 单个用户可能更喜欢其中一种或两种方法。 这将导致特定于该用户的特定于用户的行为。 但是,如果尝试从未由开发人员标记为登录屏幕的屏幕进行登录,则无论是哪个用户尝试登录,都肯定是异常的。
在我们的应用中,我们将集成一个这样的系统。 为此,我们将记录一段时间内我们应用的许多用户进行的所有登录尝试。 我们将特别注意他们尝试登录的屏幕以及它们传递给系统的数据类型。 一旦收集了很多这些样本,就可以根据用户执行的任何操作来确定系统对认证的信心。 如果系统在任何时候认为用户表现出的行为与他们的惯常行为相差很大,则该用户将未经认证并被要求验证其帐户详细信息。
让我们从创建预测模型开始,以确定用户认证是常规的还是异常的。
用于认证用户的自定义模型
我们将本节分为两个主要子节:
- 构建用于认证有效性检查的模型
- 托管自定义认证验证模型
让我们从第一部分开始。
构建用于认证有效性检查的模型
在本部分中,我们将构建模型来确定是否有任何用户正在执行常规登录或异常登录:
- 我们首先导入必要的模块,如下所示:
import sys
import os
import json
import pandas
import numpy
from keras.models import Sequential
from keras.layers import LSTM, Dense, Dropout
from keras.layers.embeddings import Embedding
from keras.preprocessing import sequence
from keras.preprocessing.text import Tokenizer
from collections import OrderedDict
- 现在,我们将数据集导入到项目中。 可以在这里中找到该数据集:
csv_file = 'data.csv'
dataframe = pandas.read_csv(csv_file, engine='python', quotechar='|', header=None)
count_frame = dataframe.groupby([1]).count()
print(count_frame)
total_req = count_frame[0][0] + count_frame[0][1]
num_malicious = count_frame[0][1]
print("Malicious request logs in dataset: {:0.2f}%".format(float(num_malicious) / total_req * 100))
前面的代码块将 CSV 数据集加载到项目中。 它还会打印一些与数据有关的统计信息,如下所示:
- 我们在上一步中加载的数据目前尚无法使用,无法进行深度学习。 在此步骤中,我们将其分为特征列和标签列,如下所示:
X = dataset[:,0]
Y = dataset[:,1]
- 接下来,我们将删除数据集中包含的某些列,因为我们不需要所有这些列来构建简单的模型:
for index, item in enumerate(X):
reqJson = json.loads(item, object_pairs_hook=OrderedDict)
del reqJson['timestamp']
del reqJson['headers']
del reqJson['source']
del reqJson['route']
del reqJson['responsePayload']
X[index] = json.dumps(reqJson, separators=(',', ':'))
- 接下来,我们将在剩余的请求正文上执行分词。 分词是一种用于将大文本块分解为较小文本的方法,例如将段落分成句子,将句子分成单词。 我们这样做如下:
tokenizer = Tokenizer(filters='\t\n', char_level=True)
tokenizer.fit_on_texts(X)
- 分词之后,我们将请求正文中的文本转换为单词向量,如下一步所示。 我们将数据集和
DataFrame标签分为两部分,即 75%-25%,以进行训练和测试:
num_words = len(tokenizer.word_index)+1
X = tokenizer.texts_to_sequences(X)
max_log_length = 1024
train_size = int(len(dataset) * .75)
X_processed = sequence.pad_sequences(X, maxlen=max_log_length)
X_train, X_test = X_processed[0:train_size], X_processed[train_size:len(X_processed)]
Y_train, Y_test = Y[0:train_size], Y[train_size:len(Y)]
- 接下来,我们基于长短期记忆(LSTM)创建基于循环神经网络(RNN)的学习方法,来识别常规用户行为。 将单词嵌入添加到层中,以帮助维持单词向量和单词之间的关系:
model = Sequential()
model.add(Embedding(num_words, 32, input_length=max_log_length))
model.add(Dropout(0.5))
model.add(LSTM(64, recurrent_dropout=0.5))
model.add(Dropout(0.5))
model.add(Dense(1, activation='sigmoid'))
我们的输出是单个神经元,在正常登录的情况下,该神经元保存0;在登录异常的情况下,则保存1。
- 现在,我们以精度作为度量标准编译模型,而损失则作为二进制交叉熵来计算:
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
print(model.summary())
- 现在,我们准备进行模型的训练:
model.fit(X_train, Y_train, validation_split=0.25, epochs=3, batch_size=128)
- 我们将快速检查模型所达到的准确率。 当前模型的准确率超过 96%:
score, acc = model.evaluate(X_test, Y_test, verbose=1, batch_size=128)
print("Model Accuracy: {:0.2f}%".format(acc * 100))
下面的屏幕快照显示了前面代码块的输出:
- 现在,我们保存模型权重和模型定义。 我们稍后将它们加载到 API 脚本中,以验证用户的认证:
model.save_weights('lstm-weights.h5')
model.save('lstm-model.h5')
现在,我们可以将认证模型作为 API 进行托管,我们将在下一部分中进行演示。
托管自定义认证验证模型
在本节中,我们将创建一个 API,用于在用户向模型提交其登录请求时对其进行认证。 请求标头将被解析为字符串,并且模型将使用它来预测登录是否有效:
- 我们首先导入创建 API 服务器所需的模块:
from sklearn.externals import joblib
from flask import Flask, request, jsonify
from string import digits
import sys
import os
import json
import pandas
import numpy
import optparse
from keras.models import Sequential, load_model
from keras.preprocessing import sequence
from keras.preprocessing.text import Tokenizer
from collections import OrderedDict
- 现在,我们实例化一个
Flask应用对象。 我们还将从上一节“构建用于认证有效性检查的模型”中加载保存的模型定义和模型权重。然后,我们重新编译模型,并使用_make_predict_function( )方法创建其预测方法,如以下步骤所示:
app = Flask(__name__)
model = load_model('lstm-model.h5')
model.load_weights('lstm-weights.h5')
model.compile(loss = 'binary_crossentropy', optimizer = 'adam', metrics = ['accuracy'])
model._make_predict_function()
- 然后,我们创建一个
remove_digits()函数,该函数用于从提供给它的输入中去除所有数字。 这将用于在将请求正文文本放入模型之前清除它:
def remove_digits(s: str) -> str:
remove_digits = str.maketrans('', '', digits)
res = s.translate(remove_digits)
return res
- 接下来,我们将在 API 服务器中创建
/login路由。 该路由由login()方法处理,并响应GET和POST请求方法。 正如我们对训练输入所做的那样,我们删除了请求标头中的非必要部分。 这可以确保模型将对数据进行预测,类似于对其进行训练的数据:
@app.route('/login', methods=['GET, POST'])
def login():
req = dict(request.headers)
item = {}
item["method"] = str(request.method)
item["query"] = str(request.query_string)
item["path"] = str(request.path)
item["statusCode"] = 200
item["requestPayload"] = []
## MORE CODE BELOW THIS LINE
## MORE CODE ABOVE THIS LINE
response = {'result': float(prediction[0][0])}
return jsonify(response)
- 现在,我们将代码添加到
login()方法中,该方法将标记请求正文并将其传递给模型以执行有关登录请求有效性的预测,如下所示:
@app.route('/login', methods=['GET, POST'])
def login():
...
## MORE CODE BELOW THIS LINE
X = numpy.array([json.dumps(item)])
log_entry = "store"
tokenizer = Tokenizer(filters='\t\n', char_level=True)
tokenizer.fit_on_texts(X)
seq = tokenizer.texts_to_sequences([log_entry])
max_log_length = 1024
log_entry_processed = sequence.pad_sequences(seq, maxlen=max_log_length)
prediction = model.predict(log_entry_processed)
## MORE CODE ABOVE THIS LINE
...
最后,应用以 JSON 字符串的形式返回其对用户进行认证的信心。
- 最后,我们使用
app的run()方法启动服务器脚本:
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000)
- 将此文件另存为
main.py。 要开始执行服务器,请打开一个新终端并使用以下命令:
python main.py
服务器监听其运行系统的所有传入 IP。 通过在0.0.0.0 IP 上运行它,可以实现这一点。 如果我们希望稍后在基于云的服务器上部署脚本,则需要这样做。 如果不指定0.0.0.0主机,则默认情况下会使它监听127.0.0.1,这不适合在公共服务器上进行部署。 您可以在此处详细了解这些地址之间的区别。
在下一节中,我们将看到如何将 ReCaptcha 集成到迄今为止在该项目中构建的应用中。 之后,我们将把本节中构建的 API 集成到应用中。
实现 ReCaptcha 来保护垃圾邮件
为了为 Firebase 认证增加另一层安全性,我们将使用 ReCaptcha。 这是 Google 所支持的一项测试,可帮助我们保护数据免受垃圾邮件和滥用行为的自动 bot 攻击。 该测试很简单,很容易被人类解决,但是却阻碍了漫游器和恶意用户的使用。
要了解有关 ReCaptcha 及其用途的更多信息,请访问这里。
ReCAPTCHA v2
在本节中,我们将把 ReCaptcha 版本 2 集成到我们的应用中。 在此版本中,向用户显示一个简单的复选框。 如果刻度变为绿色,则表明用户已通过验证。
另外,还可以向用户提出挑战,以区分人和机器人。 这个挑战很容易被人类解决。 他们要做的就是根据说明选择一堆图像。 使用 ReCaptcha 进行认证的传统流程如下所示:
一旦用户能够验证其身份,他们就可以成功登录。
获取 API 密钥
要在我们的应用内部使用 ReCaptcha,我们需要在reCAPTCHA管理控制台中注册该应用,并获取站点密钥和秘密密钥。 为此,请访问这里并注册该应用。 您将需要导航到“注册新站点”部分,如以下屏幕截图所示:
我们可以通过以下两个简单步骤来获取 API 密钥:
- 首先提供一个域名。 在这里,我们将在 reCAPTCHA v2 下选择 reCAPTCHA Android。
- 选择 Android 版本后,添加项目的包名称。 正确填写所有信息后,单击“注册”。
这将引导您到显示站点密钥和秘密密钥的屏幕,如以下屏幕快照所示:
将站点密钥和秘密密钥复制并保存到安全位置。 我们将在编码应用时使用它们。
代码整合
为了在我们的应用中包含 ReCaptcha v2,我们将使用 Flutter 包flutter_recaptcha_v2。 将flutter_recaptcha_v2:0.1.0依赖项添加到pubspec.yaml文件中,然后在终端中运行flutter packages get以获取所需的依赖项。 以下步骤详细讨论了集成:
- 我们将代码添加到
signup_signin_screen.dart。 首先导入依赖项:
import 'package:flutter_recaptcha_v2/flutter_recaptcha_v2.dart';
- 接下来,创建一个
RecaptchaV2Controller实例:
RecaptchaV2Controller recaptchaV2Controller = RecaptchaV2Controller();
- reCAPTCHA 复选框将添加为小部件。 首先,让我们定义一个返回小部件的
_createRecaptcha()方法:
Widget _createRecaptcha() {
return RecaptchaV2(
apiKey: "Your Site Key here",
apiSecret: "Your API Key here",
controller: recaptchaV2Controller,
onVerifiedError: (err){
print(err);
},
onVerifiedSuccessfully: (success) {
setState(() {
if (success) {
_signinSignup();
} else {
print('Failed to verify');
}
});
},
);
}
在上述方法中,我们仅使用RecaptchaV2()构造器,即可为特定属性指定值。 添加您先前在apiKey和apiSecret属性中注册时保存的站点密钥和秘密密钥。 我们使用先前为属性控制器创建的recaptcha控制器recaptchaV2Controller的实例。 如果成功验证了用户,则将调用_signinSignup()方法以使用户登录。如果在验证期间发生错误,我们将打印错误。
- 现在,由于在用户尝试登录时应显示
reCaptcha,因此我们将createSigninButton()中的登录凸起按钮的onPressed属性修改为recaptchaV2Controller:
Widget _createSigninButton() {
. . . . . . .
return new Padding(
. . . . . . .
child: new RaisedButton(
. . . . . .
//Modify the onPressed property
onPressed: recaptchaV2Controller.show
)
)
}
- 最后,我们将
_createRecaptcha()添加到build()内部的主体栈中:
@override
Widget build(BuildContext context) {
. . . . . . .
return new Scaffold(
. . . . . . .
body: Stack(
children: <Widget>[
_createBody(),
_createCircularProgress(),
//Add reCAPTCHA Widget
_createRecaptcha()
],
));
}
这就是一切! 现在,我们具有比 Firebase 认证更高的安全级别,可以保护应用的数据免受自动机器人的攻击。 现在让我们看一下如何集成定制模型以检测恶意用户。
在 Flutter 中部署模型
至此,我们的 Firebase 认证应用与 ReCaptcha 保护一起运行。 现在,让我们添加最后的安全层,该层将不允许任何恶意用户进入应用。
我们已经知道该模型位于以下端点。 我们只需从应用内部进行 API 调用,传入用户提供的电子邮件和密码,并从模型中获取结果值。 该值将通过使用阈值结果值来帮助我们判断登录是否是恶意的。
如果该值小于 0.20,则认为该登录名是恶意的,并且屏幕上将显示以下消息:
现在,让我们看一下在 Flutter 应用中部署模型的步骤:
- 首先,由于我们正在获取数据并且将使用网络调用(即 HTTP 请求),因此我们需要向
pubspec.yaml文件添加http依赖项,并按以下方式导入:
import 'package:http/http.dart' as http;
- 首先在
auth.dart:内部定义的BaseAuth抽象类中添加以下函数声明
Future<double> isValidUser(String email, String password);
- 现在,让我们在
Auth类中定义isValidUser()函数:
Future<double> isValidUser(String email, String password) async{
final response = await http.Client()
.get('http://34.67.160.232:8000/login?user=$email&password=$password');
var jsonResponse = json.decode(response.body);
var val = '${jsonResponse["result"]}';
double result = double.parse(val);
return result;
}
此函数将用户的电子邮件和密码作为参数,并将它们附加到请求 URL,以便为特定用户生成输出。 get request响应存储在变量响应中。 由于响应为 JSON 格式,因此我们使用json.decode()对其进行解码,并将解码后的响应存储在另一个变量响应中。 现在,我们使用‘${jsonResponse["result"]}'访问jsonResponse中的结果值,使用double.parse()将其转换为双精度类型整数,并将其存储在结果中。 最后,我们返回结果的值。
- 为了激活代码内部的恶意检测,我们从
SigninSignupScreen调用了isValidUser()方法。 当具有现有帐户的用户选择从if-else块内部登录时,将调用此方法:
if (_formMode == FormMode.SIGNIN) {
var val = await widget.auth.isValidUser(_usermail, _userpassword);
. . . .
} else {
. . . .
}
isValidUser返回的值存储在val变量中。
- 如果该值小于 0.20,则表明登录活动是恶意的。 因此,我们将异常抛出并在 catch 块内抛出
catch并在屏幕上显示错误消息。 这可以通过创建自定义异常类MalicousUserException来完成,该类在实例化时返回一条错误消息:
class MaliciousUserException implements Exception {
String message() => 'Malicious login! Please try later.';
}
- 现在,我们将在调用
isValidUser()之后添加if块,以检查是否需要抛出异常:
var val = await widget.auth.isValidUser(_usermail, _userpassword);
//Add the if block
if(val < 0.20) {
throw new MaliciousUserException();
}
- 现在,该异常已捕获在
catch块内,并且不允许用户继续登录。此外,我们将_loading设置为false以表示不需要进一步的网络操作:
catch(MaliciousUserException) {
setState(() {
_loading = false;
_errorMessage = 'Malicious user detected. Please try again later.';
});
这就是一切! 我们之前基于 Firebase 认证创建的 Flutter 应用现在可以在后台运行智能模型的情况下找到恶意用户。
总结
在本章中,我们了解了如何使用 Flutter 和由 Firebase 支持的认证系统构建跨平台应用,同时结合了深度学习的优势。 然后,我们了解了如何将黑客攻击尝试归类为一般用户行为中的异常现象,并创建了一个模型来对这些异常现象进行分类以防止恶意用户登录。最后,我们使用了 Google 的 ReCaptcha 来消除对该应用的垃圾邮件使用,因此,使其在自动垃圾邮件或脚本化黑客攻击方面更具弹性。
在下一章中,我们将探索一个非常有趣的项目–使用移动应用上的深度学习生成音乐成绩单。
七、语音/多媒体处理 - 使用 AI 生成音乐
鉴于人工智能(AI)的应用越来越多,将 AI 与音乐结合使用的想法已经存在了很长时间,并且受到了广泛的研究。 由于音乐是一系列音符,因此它是时间序列数据集的经典示例。 最近证明时间序列数据集在许多预测领域中非常有用–股市,天气模式,销售模式以及其他基于时间的数据集。 循环神经网络(RNN)是处理时间序列数据集的最多模型之一。 对 RNN 进行的流行增强称为长短期记忆(LSTM)神经元。 在本章中,我们将使用 LSTM 处理音符。
多媒体处理也不是一个新话题。 在本项目系列的早期,我们在多章中详细介绍了图像处理。 在本章中,我们将讨论并超越图像处理,并提供一个带有音频的深度学习示例。 我们将训练 Keras 模型来生成音乐样本,每次都会生成一个新样本。 然后,我们将此模型与 Flutter 应用结合使用,以通过 Android 和 iOS 设备上的音频播放器进行部署。
在本章中,我们将介绍以下主题:
- 设计项目的架构
- 了解多媒体处理
- 开发基于 RNN 的音乐生成模型
- 在 Android 和 iOS 上部署音频生成 API
让我们首先概述该项目的架构。
设计项目的架构
该项目的架构与作为应用部署的常规深度学习项目略有不同。 我们将有两组不同的音乐样本。 第一组样本将用于训练可以生成音乐的 LSTM 模型。 另一组样本将用作 LSTM 模型的随机输入,该模型将输出生成的音乐样本。 我们稍后将开发和使用的基于 LSTM 的模型将部署在 Google Cloud Platform(GCP)上。 但是,您可以将其部署在 AWS 或您选择的任何其他主机上。
下图总结了将在本项目中使用的不同组件之间的交互:
移动应用要求部署在服务器上的模型生成新的音乐样本。 该模型使用随机音乐样本作为输入,以使其通过预先训练的模型来生成新的音乐样本。 然后,新的音乐样本由移动设备获取并播放给用户。
您可以将此架构与我们之前介绍的架构进行比较,在该架构中,将有一组用于训练的数据样本,然后将模型部署在云上或本地,并用于作出预测。
我们还可以更改此项目架构,以在存在为 Dart 语言编写的 midi 文件处理库的情况下在本地部署模型。 但是,在撰写本文时,还没有与我们在开发模型时使用的 Python midi 文件库的要求兼容的稳定库。
让我们从学习多媒体处理的含义以及如何使用 OpenCV 处理多媒体文件开始。
了解多媒体处理
多媒体是几乎所有形式的视觉,听觉或两者兼有的内容的总称。 术语多媒体处理本身非常模糊。 讨论该术语的更精确方法是将其分解为两个基本部分-视觉或听觉。 因此,我们将讨论多媒体处理的术语,即图像处理和音频处理。 这些术语的混合产生了视频处理,这只是多媒体的另一种形式。
在以下各节中,我们将以单独的形式讨论它们。
图像处理
图像处理或计算机视觉是迄今为止人工智能研究最多的分支之一。 在过去的几十年中,它发展迅速,并在以下几种技术的进步中发挥了重要作用:
- 图像过滤器和编辑器
- 面部识别
- 数字绘画
- 自动驾驶汽车
我们在较早的项目中讨论了图像处理的基础知识。 在这个项目中,我们将讨论一个非常流行的用于执行图像处理的库-OpenCV。 OpenCV 是开源计算机视觉的缩写。 它由 Intel 开发,并由 Willow Garage 和 Itseez(后来被 Intel 收购)推动。 毫无疑问,由于它与所有主要的机器学习框架(例如 TensorFlow,PyTorch 和 Caffe)兼容,因此它是执行图像处理的全球大多数开发人员的首要选择。 除此之外,OpenCV 还可以使用多种语言,例如 C++,Java 和 Python。
要在 Python 环境中安装 OpenCV,可以使用以下命令:
pip install opencv-contrib-python
前面的命令将同时安装主 OpenCV 模块和contrib模块。 您可以在此处找到更多模块供您选择。 有关更多安装说明,如果前面的链接不符合您的要求,则可以在此处遵循官方文档。
让我们为您介绍一个非常简单的示例,说明如何使用 OpenCV 执行图像处理。 创建一个新的 Jupyter 笔记本,并从以下步骤开始:
- 要将 OpenCV 导入笔记本,请使用以下代码行:
import cv2
- 我们还要将 matplotlib 导入笔记本,因为如果您尝试使用本机 OpenCV 图像显示功能,Jupyter 笔记本将会崩溃:
from matplotlib import pyplot as plt
%matplotlib inline
- 让我们使用 matplotlib 为 OpenCV 的本机图像显示功能创建一个替代函数,以方便在笔记本中显示图像:
def showim(image):
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
plt.imshow(image)
plt.show()
请注意,我们将图像的配色方案从蓝色绿色红色(BGR)转换为红色绿色蓝色(RGB)。 这是由于默认情况下 OpenCV 使用 BGR 配色方案。 但是,matplotlib 在显示图片时会使用 RGB 方案,并且如果不进行这种转换,我们的图像就会显得奇怪。
- 现在,让我们将图像读取到 Jupyter 笔记本中。 完成后,我们将能够看到加载的图像:
image = cv2.imread("Image.jpeg")
showim(image)
前面代码的输出取决于您选择加载到笔记本中的图像:
在我们的示例中,我们加载了柑橘类水果切片的图像,这是艾萨克·奎萨达(Isaac Quesada)在“Unsplash”上拍摄的惊人照片。
您可以在这里找到上一张图片。
- 让我们通过将之前的图像转换为灰度图像来进行简单的操作。 为此,我们就像在声明的
showim()函数中那样简单地使用转换方法:
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
showim(gray_image)
这将产生以下输出:
- 现在让我们执行另一种常见的操作,即图像模糊。 在图像处理中通常采用模糊处理,以消除图像中信息的不必要的细节(此时)。 我们使用高斯模糊过滤器,这是在图像上创建模糊的最常见算法之一:
blurred_image = cv2.GaussianBlur(image, (7, 7), 0)
showim(blurred_image)
这将产生以下输出:
请注意,前面的图像不如原始图像清晰。 但是,它很容易达到愿意计算此图像中对象数量的目的。
- 为了在图像中定位对象,我们首先需要标记图像中的边缘。 为此,我们可以使用
Canny()方法,该方法是 OpenCV 中可用的其他选项之一,用于查找图像的边缘:
canny = cv2.Canny(blurred_image, 10, 50)
showim(canny)
这将产生以下输出:
请注意,在上图中找到的边缘数量很高。 虽然这会显示图像的细节,但是如果我们尝试对边缘进行计数以尝试确定图像中的对象数量,这将无济于事。
- 让我们尝试计算上一步生成的图像中不同项目的数量:
contours, hierarchy= cv2.findContours(canny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print("Number of objects found = ", len(contours))
上面的代码将产生以下输出:
Number of objects found = 18
但是,我们知道前面的图像中没有 18 个对象。 只有 9。因此,在寻找边缘时,我们将在canny方法中处理阈值。
- 让我们在 canny 方法中增加边缘发现的阈值。 这使得更难检测到边缘,因此仅使最明显的边缘可见:
canny = cv2.Canny(blurred_image, 50, 150)
showim(canny)
这将产生以下输出:
请注意,在柑橘类水果体内发现的边缘急剧减少,仅清晰可见其轮廓。 我们希望这会在计数时产生较少的对象。
- 让我们再次运行以下代码块:
contours, hierarchy= cv2.findContours(canny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print("Number of objects found = ", len(contours))
这将产生以下输出:
Number of objects found = 9
这是期望值。 但是,只有在特殊情况下,该值才是准确的。
- 最后,让我们尝试概述检测到的对象。 为此,我们绘制了
findContours()方法的上一步中确定的轮廓:
_ = cv2.drawContours(image, contours, -1, (0,255,0), 10)
showim(image)
这将产生以下输出:
请注意,我们已经在拍摄的原始图像中非常准确地识别出了九片水果。 我们可以进一步扩展此示例,以在任何图像中找到某些类型的对象。
要了解有关 OpenCV 的更多信息并找到一些可供学习的示例,请访问以下存储库。
现在让我们学习如何处理音频文件。
音频处理
我们已经看到了如何处理图像以及可以从中提取信息。 在本节中,我们将介绍音频文件的处理。 音频或声音是吞没您周围环境的东西。 在许多情况下,您仅能从该区域的音频剪辑中正确预测该区域或环境,而无需实际看到任何视觉提示。 声音或语音是人与人之间交流的一种形式。 安排良好的节奏模式形式的音频称为音乐,可以使用乐器制作。
音频文件的一些流行格式如下:
- MP3:一种非常流行的格式,广泛用于共享音乐文件。
- AAC:是对 MP3 格式的改进,AAC 主要用于 Apple 设备。
- WAV:由 Microsoft 和 IBM 创建,这种格式是无损压缩,即使对于小的音频文件也可能很大。
- MIDI:乐器数字接口文件实际上不包含音频。 它们包含乐器音符,因此体积小且易于使用。
音频处理是以下技术的增长所必需的:
- 用于基于语音的界面或助手的语音处理
- 虚拟助手的语音生成
- 音乐生成
- 字幕生成
- 推荐类似音乐
TensorFlow 团队的 Magenta 是一种非常流行的音频处理工具。
您可以通过这里访问 Magenta 主页。 该工具允许快速生成音频和音频文件的转录。
让我们简要地探讨 Magenta。
Magenta
Magenta 是 Google Brain 团队参与研究的一部分,该团队也参与了 TensorFlow。 它被开发为一种工具,可允许艺术家借助深度学习和强化学习算法来增强其音乐或艺术创作渠道。 这是 Magenta 的徽标:
让我们从以下步骤开始:
- 要在系统上安装 Magenta,可以使用 Python 的 pip 存储库:
pip install magenta
- 如果缺少任何依赖项,则可以使用以下命令安装它们:
!apt-get update -qq && apt-get install -qq libfluidsynth1 fluid-soundfont-gm build-essential libasound2-dev libjack-dev
!pip install -qU pyfluidsynth pretty_midi
- 要将 Magenta 导入项目中,可以使用以下命令:
import magenta
或者,按照流行的惯例,仅加载 Magenta 的音乐部分,可以使用以下命令:
import magenta.music as mm
您可以使用前面的导入在线找到很多样本。
让我们快速创作一些音乐。 我们将创建一些鼓声,然后将其保存到 MIDI 文件:
- 我们首先需要创建一个
NoteSequence对象。 在 Magenta 中,所有音乐都以音符序列的格式存储,类似于 MIDI 存储音乐的方式:
from magenta.protobuf import music_pb2
drums = music_pb2.NoteSequence()
- 创建
NoteSequence对象后,该对象为空,因此我们需要向其添加一些注解:
drums.notes.add(pitch=36, start_time=0, end_time=0.125, is_drum=True, instrument=10, velocity=80)
drums.notes.add(pitch=38, start_time=0, end_time=0.125, is_drum=True, instrument=10, velocity=80)
drums.notes.add(pitch=42, start_time=0, end_time=0.125, is_drum=True, instrument=10, velocity=80)
drums.notes.add(pitch=46, start_time=0, end_time=0.125, is_drum=True, instrument=10, velocity=80)
.
.
.
drums.notes.add(pitch=42, start_time=0.75, end_time=0.875, is_drum=True, instrument=10, velocity=80)
drums.notes.add(pitch=45, start_time=0.75, end_time=0.875, is_drum=True, instrument=10, velocity=80)
请注意,在前面的代码中,每个音符都有音高和力度。 再次类似于 MIDI 文件。
- 现在让我们为音符添加节奏,并设置音乐播放的总时间:
drums.total_time = 1.375
drums.tempos.add(qpm=60)
完成此操作后,我们现在准备导出 MIDI 文件。
- 我们首先需要将 Magenta
NoteSequence对象转换为 MIDI 文件:
mm.sequence_proto_to_midi_file(drums, 'drums_sample_output.mid')
前面的代码首先将音符序列转换为 MIDI,然后将它们写入磁盘上的drums_sample_output.mid文件。 您现在可以使用任何合适的音乐播放器播放midi文件。
继续前进,让我们探索如何处理视频。
视频处理
视频处理是多媒体处理的另一个重要部分。 通常,我们需要弄清楚移动场景中发生的事情。 例如,如果我们要生产自动驾驶汽车,则它需要实时处理大量视频才能平稳行驶。 这种情况的另一个实例可以是将手语转换为文本以帮助与语音障碍者互动的设备。 此外,需要视频处理来创建电影和动作效果。
我们将在本节中再次探讨 OpenCV。 但是,我们将演示如何在 OpenCV 中使用实时摄像机供稿来检测面部。
创建一个新的 Python 脚本并执行以下步骤:
- 首先,我们需要对脚本进行必要的导入。 这将很简单,因为我们只需要 OpenCV 模块:
import cv2
- 现在,让我们将 Haar 级联模型加载到脚本中。 Haar 级联算法是一种用于检测任何给定图像中的对象的算法。 由于视频不过是图像流,因此我们将其分解为一系列帧并检测其中的人脸:
faceCascade = cv2.CascadeClassifier("haarcascade_frontalface_default.xml")
您将不得不从以下位置获取haarcascade_frontalface_default.xml文件。
Haar 级联是一类使用级联函数执行分类的分类器算法。 保罗·维奥拉(Paul Viola)和迈克尔·琼斯(Michael Jones)引入了它们,以试图建立一种对象检测算法,该算法足够快以在低端设备上运行。 级联函数池来自几个较小的分类器。
Haar 级联文件通常以可扩展标记语言(XML)的格式找到,并且通常执行一项特定功能,例如面部检测,身体姿势检测, 对象检测等。 您可以在此处阅读有关 Haar 级联的更多信息。
- 现在,我们必须实例化摄像机以进行视频捕获。 为此,我们可以使用默认的笔记本电脑摄像头:
video_capture = cv2.VideoCapture(0)
- 现在让我们从视频中捕获帧并显示它们:
while True:
# Capture frames
ret, frame = video_capture.read()
### We'll add code below in future steps
### We'll add code above in future steps
# Display the resulting frame
cv2.imshow('Webcam Capture', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
这样您就可以在屏幕上显示实时视频供稿。 在运行此程序之前,我们需要释放相机并正确关闭窗户。
- 要正确关闭实时捕获,请使用以下命令:
video_capture.release()
cv2.destroyAllWindows()
现在,让我们对脚本进行测试运行。
您应该会看到一个窗口,其中包含您的脸部实时捕捉的图像(如果您不害羞的话)。
- 让我们向该视频提要添加面部检测。 由于用于面部检测的 Haar 级联在使用灰度图像时效果更好,因此我们将首先将每个帧转换为灰度,然后对其进行面部检测。 我们需要将此代码添加到
while循环中,如以下代码所示:
### We'll add code below in future steps
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
faces = faceCascade.detectMultiScale(
gray,
scaleFactor=1.1,
minNeighbors=5,
minSize=(30, 30),
flags=cv2.CASCADE_SCALE_IMAGE
)
### We'll add code above in future steps
这样,我们就可以检测到人脸了,因此让我们在视频供稿中对其进行标记!
- 我们将简单地使用 OpenCV 的矩形绘制函数在屏幕上标记面孔:
minNeighbors=5,
minSize=(30, 30),
flags=cv2.CASCADE_SCALE_IMAGE
)
for (x, y, w, h) in faces:
cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2)
### We'll add code above in future steps
现在让我们再次尝试运行脚本。
转到终端并使用以下命令运行脚本:
python filename.py
在这里,文件名是您保存脚本文件时的名称。
您应该获得类似于以下屏幕截图的输出:
要退出实时网络摄像头捕获,请使用键盘上的Q键(我们已在前面的代码中进行了设置)。
我们已经研究了多媒体处理的三种主要形式的概述。 现在,让我们继续前进,构建基于 LSTM 的模型以生成音频。
开发基于 RNN 的音乐生成模型
在本节中,我们将开发音乐生成模型。 我们将为此使用 RNN,并使用 LSTM 神经元模型。 RNN 与简单的人工神经网络(ANN)有很大的不同-允许在层之间重复使用输入。
虽然在 ANN 中,我们希望输入到神经网络的输入值向前移动,然后产生基于错误的反馈,并将其合并到网络权重中,但 RNN 使输入多次循环返回到先前的层。
下图表示 RNN 神经元:
从上图可以看到,通过神经元激活函数后的输入分为两部分。 一部分在网络中向前移动到下一层或输出,而另一部分则反馈到网络中。 在时间序列数据集中,可以相对于给定样本在t的时间标记每个样本,我们可以扩展前面的图,如下所示:
但是,由于通过激活函数反复暴露值,RNN 趋向于梯度消失,其中 RNN 的值逐梯度小到可以忽略不计(或在梯度爆炸的情况下变大)。 为避免这种情况,引入了 LSTM 单元,该单元通过将信息存储在单元中而允许将信息保留更长的时间。 每个 LSTM 单元由三个门和一个存储单元组成。 三个门(输入,输出和遗忘门)负责确定哪些值存储在存储单元中。
因此,LSTM 单元变得独立于 RNN 其余部分的更新频率,并且每个单元格都有自己的时间来记住它所拥有的值。 就我们而言,与其他信息相比,我们忘记了一些随机信息的时间要晚得多,这更自然地模仿了自然。
您可以在以下链接中找到有关 RNN 和 LSTM 的详细且易于理解的解释。
在开始为项目构建模型之前,我们需要设置项目目录,如以下代码所示:
├── app.py
├── MusicGenerate.ipynb
├── Output/
└── Samples/
├── 0.mid
├── 1.mid
├── 2.mid
└── 3.mid
请注意,我们已经在Samples文件夹中下载了四个 MIDI 文件样本。 然后,我们创建了要使用的MusicGenerate.ipynb Jupyter 笔记本。 在接下来的几个步骤中,我们将仅在此 Jupyter 笔记本上工作。 app.py脚本当前为空,将来,我们将使用它来托管模型。
现在让我们开始创建基于 LSTM 的用于生成音乐的模型。
创建基于 LSTM 的模型
在本节中,我们将在 Jupyter 笔记本环境中研究MusicGenerate.ipynb笔记本:
- 在此笔记本中,我们将需要导入许多模块。 使用以下代码导入它们:
import mido
from mido import MidiFile, MidiTrack, Message
from tensorflow.keras.layers import LSTM, Dense, Activation, Dropout, Flatten
from tensorflow.keras.preprocessing import sequence
from tensorflow.keras.models import Sequential
from tensorflow.keras.optimizers import Adam
from sklearn.preprocessing import MinMaxScaler
import numpy as np
我们使用了mido库。 如果您的系统上未安装它,则可以使用以下命令来安装它:
pip install mido
注意,在前面的代码中,我们还导入了 Keras 模块和子部件。 该项目中使用的 TensorFlow 版本为 2.0。 为了在您的系统上安装相同版本或升级当前的 TensorFlow 安装,可以使用以下命令:
pip install --upgrade pip
pip install --upgrade tensorflow
现在,我们将继续阅读示例文件。
- 要将 MIDI 文件读入项目笔记本,请使用以下代码:
notes = []
for msg in MidiFile('Samples/0.mid') :
try:
if not msg.is_meta and msg.channel in [0, 1, 2, 3] and msg.type == 'note_on':
data = msg.bytes()
notes.append(data[1])
except:
pass
这将在notes列表中加载通道0,1,2和3的所有开头音符。
要了解有关注解,消息和频道的更多信息,请使用以下文档。
- 由于音符处于大于 0–1 范围的可变范围内,因此我们将使用以下代码将其缩放以适合公共范围:
scaler = MinMaxScaler(feature_range=(0,1))
scaler.fit(np.array(notes).reshape(-1,1))
notes = list(scaler.transform(np.array(notes).reshape(-1,1)))
- 我们基本上拥有的是随时间变化的笔记列表。 我们需要将其转换为时间序列数据集格式。 为此,我们使用以下代码转换列表:
notes = [list(note) for note in notes]
X = []
y = []
n_prev = 20
for i in range(len(notes)-n_prev):
X.append(notes[i:i+n_prev])
y.append(notes[i+n_prev])
我们已将其转换为一个集合,其中每个样本都带有未来的 20 个音符,并且在数据集的末尾具有过去的 20 个音符。这可以通过以下方式进行:如果我们有 5 个样本,例如M[1],M[2],M[3],M[4]和M[5],然后我们将它们安排在大小为 2 的配对中(类似于我们的 20),如下所示:
M[1] M[2]M[2] M[3]M[3] M[4],依此类推
- 现在,我们将使用 Keras 创建 LSTM 模型,如以下代码所示:
model = Sequential()
model.add(LSTM(256, input_shape=(n_prev, 1), return_sequences=True))
model.add(Dropout(0.3))
model.add(LSTM(128, input_shape=(n_prev, 1), return_sequences=True))
model.add(Dropout(0.3))
model.add(LSTM(256, input_shape=(n_prev, 1), return_sequences=False))
model.add(Dropout(0.3))
model.add(Dense(1))
model.add(Activation('linear'))
optimizer = Adam(lr=0.001)
model.compile(loss='mse', optimizer=optimizer)
随意使用此 LSTM 模型的超参数。
- 最后,我们将训练样本适合模型并保存模型文件:
model.fit(np.array(X), np.array(y), 32, 25, verbose=1)
model.save("model.h5")
这将在我们的项目目录中创建model.h5文件。 每当用户从应用发出生成请求时,我们都会将此文件与其他音乐样本一起使用,以随机生成新的乐曲。
现在,让我们使用 Flask 服务器部署此模型。
使用 Flask 部署模型
对于项目的这一部分,您可以使用本地系统,也可以在其他地方的app.py中部署脚本。 我们将编辑此文件以创建 Flask 服务器,该服务器生成音乐并允许下载生成的 MIDI 文件。
该文件中的某些代码与 Jupyter 笔记本类似,因为每次加载音频样本并将其与我们生成的模型一起使用时,音频样本始终需要进行类似的处理:
- 我们使用以下代码将所需的模块导入此脚本:
import mido
from mido import MidiFile, MidiTrack, Message
from tensorflow.keras.models import load_model
from sklearn.preprocessing import MinMaxScaler
import numpy as np
import random
import time
from flask import send_file
import os
from flask import Flask, jsonify
app = Flask(__name__)
请注意,我们进行的最后四次导入与之前在 Jupyter 笔记本中导入的内容不同。 同样,我们不需要将几个 Keras 组件导入此脚本,因为我们将从已经准备好的模型中加载。
在上一个代码块的最后一行代码中,我们实例化了一个名为app的 Flask 对象。
- 在此步骤中,我们将创建函数的第一部分,当在 API 上调用
/generate路由时,该函数将生成新的音乐样本:
@app.route('/generate', methods=['GET'])
def generate():
songnum = random.randint(0, 3)
### More code below this
- 一旦我们随机决定在音乐生成过程中使用哪个样本文件,我们就需要像 Jupyter 笔记本中的训练样本那样对它进行类似的转换:
def generate():
.
.
.
notes = []
for msg in MidiFile('Samples/%s.mid' % (songnum)):
try:
if not msg.is_meta and msg.channel in [0, 1, 2, 3] and msg.type == 'note_on':
data = msg.bytes()
notes.append(data[1])
except:
pass
scaler = MinMaxScaler(feature_range=(0, 1))
scaler.fit(np.array(notes).reshape(-1, 1))
notes = list(scaler.transform(np.array(notes).reshape(-1, 1)))
### More code below this
在前面的代码块中,我们加载了示例文件,并从训练过程中使用的相同通道中提取了其注解。
- 现在,我们将像在训练期间一样缩放音符:
def generate():
.
.
.
notes = [list(note) for note in notes]
X = []
y = []
n_prev = 20
for i in range(len(notes) - n_prev):
X.append(notes[i:i + n_prev])
y.append(notes[i + n_prev])
### More code below this
我们也将这些笔记列表转换为适合模型输入的形状,就像我们在训练过程中对输入所做的一样。
- 接下来,我们将使用以下代码来加载 Keras 模型并从该模型创建新的注解列表:
def generate():
.
.
.
model = load_model("model.h5")
xlen = len(X)
start = random.randint(0, 100)
stop = start + 200
prediction = model.predict(np.array(X[start:stop]))
prediction = np.squeeze(prediction)
prediction = np.squeeze(scaler.inverse_transform(prediction.reshape(-1, 1)))
prediction = [int(i) for i in prediction]
### More code below this
- 现在,我们可以使用以下代码将此音符列表转换为 MIDI 序列:
def generate():
.
.
.
mid = MidiFile()
track = MidiTrack()
t = 0
for note in prediction:
vol = random.randint(50, 70)
note = np.asarray([147, note, vol])
bytes = note.astype(int)
msg = Message.from_bytes(bytes[0:3])
t += 1
msg.time = t
track.append(msg)
mid.tracks.append(track)
### More code below this
- 现在,我们准备将文件保存到磁盘。 它包含从模型随机生成的音乐:
def generate():
.
.
.
epoch_time = int(time.time())
outputfile = 'output_%s.mid' % (epoch_time)
mid.save("Output/" + outputfile)
response = {'result': outputfile}
return jsonify(response)
因此,/generate API 以 JSON 格式返回生成的文件的名称。 然后,我们可以下载并播放此文件。
- 要将文件下载到客户端,我们需要使用以下代码:
@app.route('/download/<fname>', methods=['GET'])
def download(fname):
return send_file("Output/"+fname, mimetype="audio/midi", as_attachment=True)
请注意,前面的函数在/download/filename路由上起作用,在该路由上,客户端根据上一代 API 调用的输出提供文件名。 下载的文件的 MIME 类型为audio/midi,它告诉客户端它是 MIDI 文件。
- 最后,我们可以添加将执行此服务器的代码:
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)
完成此操作后,我们可以在终端中使用以下命令来运行服务器:
python app.py
如果代码中产生任何警告,您将从控制台获得一些调试信息。 完成此操作后,我们准备在下一节中为我们的 API 构建 Flutter 应用客户端。
在 Android 和 iOS 上部署音频生成 API
成功创建和部署模型后,现在开始构建移动应用。 该应用将用于获取和播放由先前创建的模型生成的音乐。
它将具有三个按钮:
- 生成音乐:生成新的音频文件
- 播放:播放新生成的文件
- 停止:停止正在播放的音乐
另外,它的底部将显示一些文本,以显示应用的当前状态。
该应用将显示如下:
该应用的小部件树如下所示:
现在开始构建应用的 UI。
创建 UI
我们首先创建一个新的 Dart 文件play_music.dart和一个有状态的小部件PlayMusic。 如前所述,在该文件中,我们将创建三个按钮来执行基本功能。 以下步骤描述了如何创建 UI:
- 定义
buildGenerateButton()方法以创建RaisedButton变量,该变量将用于生成新的音乐文件:
Widget buildGenerateButton() {
return Padding(
padding: EdgeInsets.only(left: 16, right: 16, top: 16),
child: RaisedButton(
child: Text("Generate Music"),
color: Colors.blue,
textColor: Colors.white,
),
);
}
在前面定义的函数中,我们创建一个RaisedButton,并添加Generate Music文本作为子元素。 color属性的Colors.blue值用于为按钮赋予蓝色。 另外,我们将textColor修改为Colors.white,以使按钮内的文本为白色。 使用EdgeInsets.only()给按钮提供左,右和顶部填充。 在后面的部分中,我们将在按钮上添加onPressed属性,以便每次按下按钮时都可以从托管模型中获取新的音乐文件。
- 定义
buildPlayButton()方法以播放新生成的音频文件:
Widget buildPlayButton() {
return Padding(
padding: EdgeInsets.only(left: 16, right: 16, top: 16),
child: RaisedButton(
child: Text("Play"),
onPressed: () {
play();
},
color: Colors.blue,
textColor: Colors.white,
),
);
}
在前面定义的函数中,我们创建一个RaisedButton,并添加"Play"文本作为子元素。 color属性的Colors.blue值用于为按钮赋予蓝色。 另外,我们将textColor修改为Colors.white,以使按钮内的文本为白色。 使用EdgeInsets.only()给按钮提供左,右和顶部填充。 在后面的部分中,我们将在按钮上添加onPressed属性,以在每次按下按钮时播放新生成的音乐文件。
- 定义
buildStopButton()方法以停止当前正在播放的音频:
Widget buildStopButton() {
return Padding(
padding: EdgeInsets.only(left: 16, right: 16, top: 16),
child: RaisedButton(
child: Text("Stop"),
onPressed: (){
stop();
},
color: Colors.blue,
textColor: Colors.white,
)
);
}
在前面定义的函数中,我们创建一个RaisedButton,并添加"Stop"文本作为子元素。 color属性的Colors.blue值用于为按钮赋予蓝色。 另外,我们将textColor修改为Colors.white,以使按钮内的文本为白色。 使用EdgeInsets.only()给按钮提供左,右和顶部填充。 在下一节中,我们将向按钮添加onPressed属性,以在按下按钮时停止当前播放的音频。
- 覆盖
PlayMusicState中的build()方法,以创建先前创建的按钮的Column:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Generate Play Music"),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
buildGenerateButton(),
buildPlayButton(),
buildStopButton(),
],
)
);
}
在前面的代码片段中,我们返回Scaffold。 它包含一个AppBar,其中具有[Generate Play Music]作为title。 Scaffold的主体是Column。 列的子级是我们在上一步中创建的按钮。 通过调用相应方法将按钮添加到该列中。 此外,crossAxisAlignment属性设置为CrossAxisAlignment.stretch,以便按钮占据父容器(即列)的总宽度。
此时,该应用如下所示:
在下一节中,我们将添加一种在应用中播放音频文件的机制。
添加音频播放器
创建应用的用户界面后,我们现在将音频播放器添加到应用中以播放音频文件。 我们将使用audioplayer插件添加音频播放器,如下所示:
- 我们首先将依赖项添加到
pubspec.yaml文件中:
audioplayers: 0.13.2
现在,通过运行flutter pub get获得包。
- 接下来,我们将插件导入
play_music.dart。
import 'package:audioplayers/audioplayers.dart';
- 然后,在
PlayMusicState内创建AudioPlayer的实例:
AudioPlayer audioPlayer = AudioPlayer();
- 现在,让我们定义一个
play()方法来播放远程可用的音频文件,如下所示:
play() async {
var url = 'http://34.70.80.18:8000/download/output_1573917221.mid';
int result = await audioPlayer.play(url);
if (result == 1) {
print('Success');
}
}
最初,我们将使用存储在url变量中的样本音频文件。 通过传递url中的值,使用audioPlayer.play()播放音频文件。 另外,如果从url变量成功访问和播放了音频文件,则结果将存储在结果变量中,其值将为1。
- 现在,将
onPressed属性添加到buildPlayButton内置的播放按钮中,以便每当按下该按钮时就播放音频文件:
Widget buildPlayButton() {
return Padding(
padding: EdgeInsets.only(left: 16, right: 16, top: 16),
child: RaisedButton(
....
onPressed: () {
play();
},
....
),
);
}
在前面的代码片段中,我们添加onPressed属性并调用play()方法,以便每当按下按钮时就播放音频文件。
- 现在,我们将定义
stop()以停止正在播放的音乐:
void stop() {
audioPlayer.stop();
}
在stop()方法内部,我们只需调用audioPlayer.stop()即可停止正在播放的音乐。
- 最后,我们为
buildStopButton()中内置的停止按钮添加onPressed属性:
Widget buildStopButton() {
return Padding(
padding: EdgeInsets.only(left: 16, right: 16, top: 16),
child: RaisedButton(
....
onPressed: (){
stop();
},
....
)
);
}
在前面的代码片段中,我们向onPressed中的stop()添加了一个调用,以便一旦按下停止按钮就停止音频。
现在开始使用 Flutter 应用部署模型。
部署模型
在为应用成功添加基本的播放和停止功能之后,现在让我们访问托管模型以每次生成,获取和播放新的音频文件。 以下步骤详细讨论了如何在应用内部访问模型:
- 首先,我们定义
fetchResponse()方法来生成和获取新的音频文件:
void fetchResponse() async {
final response =
await http.get('http://35.225.134.65:8000/generate');
if (response.statusCode == 200) {
var v = json.decode(response.body);
fileName = v["result"] ;
} else {
throw Exception('Failed to load');
}
}
我们首先使用http.get()从 API 获取响应,然后传入托管模型的 URL。 get()方法的响应存储在response变量中。 get()操作完成后,我们使用response.statusCode检查状态码。 如果状态值为200,则获取成功。 接下来,我们使用json.decode()将响应的主体从原始 JSON 转换为Map<String,dynamic>,以便可以轻松访问响应主体中包含的键值对。 我们使用v["result"]访问新音频文件的值,并将其存储在全局fileName变量中。 如果responseCode不是200,我们只会抛出一个错误。
- 现在让我们定义
load()以对fetchResponse()进行适当的调用:
void load() {
fetchResponse();
}
在前面的代码行中,我们仅定义一个load()方法,该方法用于调用fetchResponse()来获取新生成的音频文件的值。
- 现在,我们将修改
buildGenerateButton()中的onPressed属性,以每次生成新的音频文件:
Widget buildGenerateButton() {
return Padding(
....
child: RaisedButton(
....
onPressed: () {
load();
},
....
),
);
}
根据应用的功能,每当按下生成按钮时,都应生成一个新的音频文件。 这直接意味着无论何时按下“生成”按钮,我们都需要调用 API 以获取新生成的音频文件的名称。 因此,我们修改buildGenerateButton()以添加onPressed属性,以便每当按下按钮时,它都会调用load(),该调用随后将调用fetchResponse()并将新音频文件的名称存储在输出中。
- 托管的音频文件有两个部分,
baseUrl和fileName。baseUrl对于所有调用均保持不变。 因此,我们声明一个存储baseUrl的全局字符串变量:
String baseUrl = 'http://34.70.80.18:8000/download/';
回想一下,我们已经在“步骤 1”中将新音频文件的名称存储在fileName中。
- 现在,让我们修改
play()以播放新生成的文件:
play() async {
var url = baseUrl + fileName;
AudioPlayer.logEnabled = true;
int result = await audioPlayer.play(url);
if (result == 1) {
print('Success');
}
}
在前面的代码片段中,我们修改了前面定义的play()方法。 我们通过附加baseUrl和fileName创建一个新的 URL,以便url中的值始终与新生成的音频文件相对应。 我们在调用audioPlayer.play()时传递 URL 的值。 这样可以确保每次按下播放按钮时,都会播放最新生成的音频文件。
- 此外,我们添加了
Text小部件以反映文件生成状态:
Widget buildLoadingText() {
return Center(
child: Padding(
padding: EdgeInsets.only(top: 16),
child: Text(loadText)
)
);
}
在前面定义的函数中,我们创建了一个简单的Text小部件,以反映提取操作正在运行以及何时完成的事实。 Text小部件具有顶部填充,并与Center对齐。 loadText值用于创建窗口小部件。
全局声明该变量,其初始值为'Generate Music':
String loadText = 'Generate Music';
- 更新
build()方法以添加新的Text小部件:
@override
Widget build(BuildContext context) {
return Scaffold(
....
body: Column(
....
children: <Widget>[
buildGenerateButton(),
....
buildLoadingText()
],
)
);
}
现在,我们更新build()方法以添加新创建的Text小部件。 该窗口小部件只是作为先前创建的Column的子级添加的。
- 当用户想要生成一个新的文本文件时,并且在进行提取操作时,我们需要更改文本:
void load() {
setState(() {
loadText = 'Generating...';
});
fetchResponse();
}
在前面的代码段中,loadText值设置为'Generating...',以反映正在进行get()操作的事实。
- 最后,获取完成后,我们将更新文本:
void fetchResponse() async {
final response =
await http.get('http://35.225.134.65:8000/generate').whenComplete((){
setState(() {
loadText = 'Generation Complete';
});
});
....
}
提取完成后,我们将loadText的值更新为'Generation Complete'。 这表示应用现在可以播放新生成的文件了。
在使应用的所有部分正常工作之后,现在让我们通过创建最终的材质应用将所有内容放在一起。
创建最终的材质应用
现在创建main.dart文件。 该文件包含无状态窗口小部件MyApp。 我们重写build()方法并将PlayMusic设置为其子级:
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: PlayMusic(),
);
}
在覆盖的build()方法中,我们简单地将home创建为PlayMusic()的MaterialApp。
总结
在本章中,我们通过将多媒体处理分解为图像,音频和视频处理的核心组件来进行研究,并讨论了一些最常用的处理工具。 我们看到了使用 OpenCV 执行图像或视频处理变得多么容易。 另外,我们看到了一个使用 Magenta 生成鼓音乐的简单示例。 在本章的下半部分,我们介绍了 LSTM 如何与时间序列数据一起使用,并构建了一个 API,该 API 可以从提供的样本文件生成器乐。 最后,我们将此 API 与 Flutter 应用结合使用,该应用是跨平台的,可以同时部署在 Android,iOS 和 Web 上。
在下一章中,我们将研究如何使用深度强化学习(DRL)来创建可以玩棋盘游戏(例如国际象棋)的智能体。