🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎
📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝
📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】 深度学习【DL】
🖍foreword
✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。
如果你对这个系列感兴趣的话,可以关注订阅哟👋
文章目录
在上一章中,我们了解了如何在数据点数量最少的情况下利用新颖的架构。在本章中,我们将换位思考,了解如何将卷积神经网络( CNN ) 与广泛使用的循环神经网络( RNN ) 家族中的算法结合使用(截至撰写本文时)书)在自然语言处理( NLP ) 中开发利用计算机视觉和 NLP 的解决方案。
为了理解 CNN 和 RNN 的结合,我们将首先了解 RNN 的工作原理及其变体——主要是长短期记忆(LSTM)——以了解它们如何应用于预测给定图像作为输入的注释。之后,我们将了解另一个重要的损失函数,称为连接主义时间分类 ( CTC ) l oss 函数,然后将其与 CNN 和 RNN 结合使用以执行手写图像的转录。最后,我们将了解并利用 Transformer 使用带有 Transformers 的检测( DETR ) 架构执行对象检测。
在本章结束时,您将了解以下主题:
- 介绍 RNN
- 介绍 LSTM 架构
- 实现图像字幕
- 转录手写图像
- 使用 DETR 进行对象检测
介绍 RNN
一个 RNN 可以有多种架构。构建 RNN 的一些可能方法如下:
在上图中,底部的框是输入,然后是隐藏层(中间的框),然后顶部的框是输出层。一对一架构是典型的神经网络,在输入和输出层之间有一个隐藏层。不同架构的示例如下:
- 一对多:输入是图像,输出是图像的标题。
- 多对一:输入是电影评论(输入中的多个单词),输出是与评论相关的情绪。
- 多对多:将一种语言的句子机器翻译成另一种语言的句子。
需要 RNN 架构背后的想法
当我们想要在给定事件序列的情况下预测下一个事件时,RNN 很有用。一个例子可以是预测后面的单词:这是一个___。
假设在现实中,句子是This is an example。
传统的文本挖掘技术将通过以下方式解决问题:
1.对每个单词进行编码,同时为潜在的新单词附加索引:
This: {1,0,0,0}
is: {0,1,0,0}
an: {0,0,1,0}
2.编码短语This is an :
This is an: {1,1,1,0}
3.创建训练数据集:
输入 --> {1,1,1,0}
输出 --> {0,0,0,1}
4.使用给定的输入和输出组合构建模型:
该模型的主要缺点之一是输入表示在输入句子中不会改变,无论其形式是this is an、an is this还是this an is。
但是,直观地,我们知道前面的每个句子都是不同的,不能用相同的数学结构来表示。这需要一个不同的架构,如下所示:
在前面的架构中,句子中的每个单词都会在输入框中输入一个单独的框。这确保了我们保留输入句子的结构;例如this进入第一个框,is进入第二个框,an进入第三个框。顶部的输出框将是输出——即example。
了解 RNN 架构的必要性后,在下一节中,让我们了解如何解释 RNN 的输出。
探索 RNN 的结构
您可以将 RNN 视为一种保存内存的机制——隐藏层包含内存。RNN 的展开版本如下:
右侧的网络是左侧网络的展开版本。右边的网络在每个时间步都接受一个输入,并在每个时间步提取输出。
请注意,在预测第三个时间步的输出时,我们通过隐藏层合并前两个时间步的值,隐藏层连接跨时间步的值。
让我们探索前面的图表:
- u 权重表示将输入层连接到隐藏层的权重。
- w 权重表示隐藏层到隐藏层的连接。
- v 权重表示隐藏层到输出层的连接。
给定时间步的输出取决于当前时间步的输入和前一个时间步的隐藏层值。通过引入前一个时间步的隐藏层作为输入,连同当前时间步的输入,我们正在从前一个时间步获取信息。通过这种方式,我们正在创建一个启用内存存储的连接管道。
为什么要存储内存?
需要存储内存,因为在前面的示例中,或者甚至在一般的文本生成中,下一个单词不仅取决于前面的单词,还取决于要预测的单词之前的单词的上下文。
鉴于我们正在查看前面的单词,应该有一种方法可以将它们保存在内存中,以便我们可以更准确地预测下一个单词。
我们也应该有秩序的记忆;通常,最近的单词在预测下一个单词时比离要预测的单词更远的单词更有用。
考虑到多个时间步来进行预测的传统 RNN 可视化如下:
请注意,随着时间步长的增加,较早时间步长(时间步长 1)出现的输入对较晚时间步长(时间步长 7)的输出的影响会降低。可以在此处看到一个示例(暂时,让我们忽略偏差项并假设在时间步 1 的隐藏层输入是0并且我们在时间步 5 - h 5预测隐藏层的值):
可以看到,随着时间步长的增加,如果U >1,则隐藏层 ( h 5 ) 的值高度依赖于X 1 ;然而,如果U <1 ,它对X 1的依赖性要小得多。
对U矩阵的依赖也会导致隐藏层( h 5 )的值非常小,因此在U的值非常小时会导致梯度消失,在U的值非常高时会导致梯度爆炸.
当长期依赖于预测下一个单词时,上述现象会导致问题。为了解决这个问题,我们将使用 LSTM 架构。
介绍 LSTM 架构
在上一节中,我们了解了传统 RNN 如何面临梯度消失或爆炸的问题,导致它无法容纳长期记忆。在本节中,我们将学习如何利用 LSTM 来解决这个问题。
为了通过示例进一步理解场景,让我们考虑以下句子:
我来自英格兰。我说__。
在前面的句子中,直观地,我们知道大多数来自英格兰的人都会说英语。要填写的空白值(英语)是从该人来自英国的事实获得的。虽然在这种情况下,我们的信号词(英格兰)更接近空白值,但在现实场景中,我们可能会发现信号词远离空白空间(我们试图预测的词)。当信号词和空白值之间的距离很大时,由于梯度消失或爆炸现象,通过传统 RNN 进行的预测可能会出错。LSTM 解决了这种情况——我们将在下一节中了解这一点。
LSTM的工作细节
一个标准的 LSTM 架构如下:
在上图中,您可以看到,虽然输入X和输出h仍然与我们在探索 RNN 的结构部分中看到的相似,但在 LSTM 中输入和输出之间发生的计算是不同的。让我们了解输入和输出之间发生的各种激活:
在上图中,我们可以观察到以下内容:
- X和h表示时间步t的输入和输出。
- C代表细胞状态。这可能有助于存储长期记忆。
- C t-1是从前一个时间步转移的单元状态。
- h t-1 表示上一个时间步的输出。
- f t表示有助于忘记某些信息的激活。
- i t表示对应于输入与前一个时间步的输出(h t-1)相结合的变换。
需要遗忘的内容f t得到如下:
请注意,W xf和W hf分别表示与输入和前一个隐藏层相关的权重。
单元状态通过将来自前一个时间步长C t-1的单元状态乘以有助于遗忘的输入内容来更新:f t。
更新后的cell状态如下:
请注意,在前面的步骤中,我们在C t-1和f t之间执行元素到元素的乘法,以获得修改后的单元状态C t。
为了理解上述操作的帮助,让我们看一下输入语句:I am from England。我说__。
在下一步中,我们将包括从当前时间步到单元状态以及输出的附加信息。修改后的单元状态(在忘记要忘记的内容之后)由输入激活(基于当前时间步的输入以及前一个时间步的输出)和调制门g t(有助于识别单元状态要更新的量)。
输入激活计算如下:
请注意,W xi和W hi分别表示与输入和前一个隐藏层相关的权重。
修改后的门的激活计算如下:
请注意,W xg和W hg分别表示与输入和前一个隐藏层相关的权重。
修改后的单元状态C t将传递到下一个时间步,现在如下:
最后,我们将激活的更新单元状态 ( tanh(C t ) ) 乘以激活的输出值O t ,以获得时间步t的最终输出h t:
这样,我们可以利用 LSTM 中存在的各种门来选择性地记住过长的时间步长。
在 PyTorch 中实现 LSTM
在典型的文本相关练习中,每个单词都是 LSTM 的输入——每个时间步一个单词。为了让 LSTM 工作,我们执行以下两个步骤:
- 将每个单词转换为嵌入向量。
- 将时间步中相关单词对应的嵌入向量作为输入传递给 LSTM。
让我们了解我们必须将输入单词转换为嵌入向量的原因。如果我们的词汇表中有 10 万个独特的单词,我们必须在将它们传递到网络之前对它们进行一次热编码。但是,为每个单词创建一个 one-hot-encoded 向量会丢失单词的语义含义——例如,like和enjoy是相似的并且应该具有相似的向量。为了解决这种情况,我们利用词嵌入,这有助于自动学习词向量表示(因为它们是网络的一部分)。词嵌入的获取方式如下:
embed = nn.Embedding(vocab_size, embed_size)
在前面的代码中,该nn.Embedding方法将vocab_size维数作为输入并返回embed_size输出的维数。这样,如果词汇表大小为 100K,嵌入大小为 128,则 100K 单词中的每一个都表示为 128 维向量。执行此练习的一个好处是,一般而言,相似的单词将具有相似的嵌入。
接下来,我们通过 LSTM 传递词嵌入。LSTM在PyTorch中使用nn.LSTM方法实现,如下:
-
hidden_state, cell_state
= nn.LSTM(embed_
size, \
-
hidden_
size, num_layers)
上述代码中,embed_size表示每个时间步对应的嵌入大小,hidden_size对应隐藏层输出的维度,num_layers表示LSTM相互堆叠的次数。
此外,该nn.LSTM方法返回隐藏状态值和单元状态值。
现在我们了解了 LSTM 和 RNN 的工作细节,让我们在下一节中了解如何在预测给定图像的字幕时将它们与 CNN 结合使用。
实现图像字幕
图像字幕是指在给定图像的情况下生成字幕。在本节中,我们将首先了解构建可以在给定图像的情况下生成文本标题的 LSTM 的预处理,然后将学习如何结合 CNN 和 LSTM 来执行图像标题。在我们了解构建生成字幕的系统之前,让我们了解示例输入和输出的外观:
在前面的示例中,图像是输入,预期输出是图像的标题——在这张图像中,我可以看到几根蜡烛。背景为黑色。
我们将采取的解决这个问题的策略如下:
- 预处理输出(基本事实注释/标题),以便每个唯一的单词都由唯一的 ID 表示。
- 鉴于输出句子可以是任意长度,让我们分配一个开始和结束标记,以便模型知道何时停止生成预测。此外,确保所有输入句子都被填充,以便所有输入具有相同的长度。
- 将输入图像通过预训练模型,例如 VGG16、ResNet-18 等,在扁平化层之前获取特征。
- 使用图像的特征图与上一步获得的文本(如果是我们要预测的第一个词,则为起始标记)来预测一个词。
- 重复前面的步骤,直到我们获得结束令牌。
既然我们已经理解了在高层次上要做什么,让我们在下一节的代码中实现前面的步骤。
代码中的图像字幕
让我们在代码中执行上一节设计的策略:
1.从 Open Images 数据集中获取数据集,其中包括训练图像、它们的注释和验证数据集:
- 导入相关包,定义设备,并获取包含要下载的图像信息的 JSON 文件:
-
!pip install -qU openimages torch_snippets urllib
3
-
!wget -O
open_images_train_captions.jsonl -q https:
/
/storage.googleapis.com
/localized-narratives
/annotations
/
open_images_train_v
6_captions.jsonl
-
from torch_snippets import
*
-
import json
-
device
=
'cuda'
if torch.cuda.
is_available()
else
'cpu'
- 遍历 JSON 文件的内容并获取前 100,000 张图像的信息:
-
with
open(
'open_images_train_captions.jsonl',
'r')
as \
-
json_
file:
-
json_list
= json_
file.
read().split(
'\n')
-
np.
random.shuffle(json_list)
-
data
= []
-
N
=
100000
-
for ix, json_str
in Tqdm(enumerate(json_list), N):
-
if ix
=
= N: break
-
try:
-
result
= json.loads(json_str)
-
x
= pd.DataFrame.
from_dict(result, orient
=
'index').T
-
data.append(x)
-
except:
-
pass
从 JSON 文件中获取的信息示例如下:
从前面的示例中,我们可以看到,caption并且image_id是我们将在后续步骤中使用的关键信息。image_id将用于获取相应的图像,caption并将用于关联与从给定图像 ID 获得的图像对应的输出。
- 将数据框 ( data) 拆分为训练和验证数据集:
-
np.
random.seed(
10)
-
data
= pd.concat(
data)
-
data[
'train']
= np.
random.choice([
True,
False], \
-
size
=len(
data),p
=[
0.95,0.05] )
-
data.
to_csv(
'data.csv',
index
=
False)
- 下载与从 JSON 文件中获取的图像 ID 对应的图像:
-
from openimages.download import _download_images_
by_id
-
!mkdir -p train-images val-images
-
subset_imageIds
=
data[
data[
'train']].image_id.tolist()
-
_download_images_
by_id(subset_imageIds,
'train', \
-
'./train-images/')
-
-
subset_imageIds
=
data[~
data[
'train']].image_id.tolist()
-
_download_images_
by_id(subset_imageIds,
'train', \
-
'./val-images/')
2.创建数据框中所有标题中存在的所有唯一单词的词汇表:
- 词汇表对象可以将所有字幕中的每个单词映射到一个唯一的整数,反之亦然。我们将利用torchtext库的Field.build_vocab功能,它遍历所有单词(注释/标题)并将它们累积到两个计数器中,stoi和itos,它们分别是“string to int”(字典)和“int to string”(一个列表):
-
from torchtext.
data import Field
-
from pycocotools.coco import COCO
-
from collections import defaultdict
-
-
captions
= Field(
sequential
=
False, init_token
=
'<start>', \
-
eos_token
=
'<end>')
-
all_captions
=
data[
data[
'train' ]][
'caption'].tolist()
-
all_tokens
= [[w.lower()
for w
in c.split()] \
-
for c
in
all_captions]
-
all_tokens
= [w
for sublist
in
all_tokens \
for
-
w
in sublist]
-
.build_vocab(
all_tokens)
在前面的代码中,Fieldforcaptions是一个专门的对象,用于在 PyTorch 中构建更复杂的 NLP 数据集。我们不能像处理图像那样直接处理文本,因为字符串与张量不兼容。因此,我们需要跟踪所有唯一出现的单词(也称为标记),这将有助于每个单词与唯一关联整数的一对一映射。例如,如果输入标题是Cat sat on the mat,基于单词到整数的映射,序列将被转换为,比如说,[5 23 24 4 29],其中cat与整数 5 唯一关联。这种映射通常称为词汇表,可能看起来像 {'<pad>': 0, '<unk'>: 1, '<start>': 2, '<end>': 3, 'the': 4, 'cat': 5, ...., 'on': 24, 'sat': 23, ... }. 前几个标记保留用于特殊功能,例如填充、未知、句子的开头和句子的结尾。
- 我们只需要captions词汇组件,所以在下面的代码中,我们创建了一个虚拟vocab对象,它是轻量级的,并且会有一个额外的<pad>标记在captions.vocab:
-
class Vocab: pass
-
vocab
= Vocab()
-
captions.vocab.itos.insert(
0,
'<pad>')
-
vocab.itos
= captions.vocab.itos
-
-
vocab.stoi
= defaultdict(lambda: \
-
captions.vocab.itos.
index(
'<unk>'))
-
vocab.stoi[
'<pad>']
=
0
-
for s,i
in captions.vocab.stoi.items():
-
vocab.stoi[s]
= i
+
1
请注意,它vocab.stoi被定义为defaultdict具有默认功能。当键不存在时,Python 使用这个特殊的字典返回一个默认值。'<unk>'在我们的例子中,当我们尝试调用时,我们将返回一个令牌vocab.stoi[<new-key/word>]。这在验证阶段很方便,其中可能存在一些训练数据中不存在的令牌。
3.定义数据集类 - CaptioningDataset:
- 定义__init__方法,我们提供之前获得的数据帧 ( df)、包含图像的文件夹 ( root)vocab和图像转换管道 ( self.transform):
-
from torchvision import transforms
-
class CaptioningData(Dataset):
-
def __init__(
self, root, df, vocab):
-
self.df
= df.
reset_
index(drop
=
True)
-
self.root
= root
-
self.vocab
= vocab
-
self.transform
= transforms. Compose([
-
transforms.Resize(
224),
-
transforms.RandomCrop(
224),
-
transforms.RandomHorizontalFlip(),
-
transforms.ToTensor(),
-
transforms.Normalize((
0.485,
0.456,
0.406),
-
(
0.229,
0.224,
0.225))]
-
)
- 定义__getitem__获取图像及其相应标题的方法。vocab此外,使用在上一步中构建的目标将目标转换为相应单词 ID 的列表:
-
def
__getitem__(
self, index):
-
"""Returns one data pair (image and caption)."""
-
row = self.df.iloc[index].squeeze()
-
id = row.image_id
-
image_path =
f'{self.root}/{id}.jpg'
-
image = Image.
open(os.path.join(image_path))\
-
.convert(
'RGB')
-
-
caption = row.caption
-
tokens =
str(caption).lower().split()
-
target = []
-
target.append(vocab.stoi[
'<start>'])
-
target.extend([vocab.stoi[token]
for token
in tokens])
-
target.append(vocab.stoi[
'<end>'])
-
target = torch.Tensor(target).long()
-
return image, target, caption
- 定义__choose__方法:
-
def
choose(
self):
-
return
self[np.random.randint(len(
self))]
- 定义__len__方法:
-
def
__len__(
self):
-
return len(
self.df)
- 定义collate_fn处理一批数据的方法:
-
def collate_fn(
self,
data):
-
data.
sort(
key
=lambda x: len(x[
1]), reverse
=
True)
-
images, targets, captions
= zip(
*
data)
-
images
= torch.stack([
self.transform(image) \
-
for image
in images],
0)
-
lengths
= [len(tar)
for tar
in targets]
-
_targets
= torch.
zeros(len(captions), \
-
max(lengths)).long()
-
for i, tar
in enumerate(targets):
-
end
= lengths[i]
-
_targets[i, :
end]
= tar[:
end]
-
return images.
to(device), _targets.
to(device), \
-
torch.tensor(lengths).long().
to(device)
在该collate_fn方法中,我们正在计算批次中字幕的最大长度(具有最大字数的字幕),并将批次中的其余字幕填充为相同的长度。
4.定义训练和验证数据集和数据加载器:
-
trn_ds
= CaptioningData(
'train-images',
data[
data[
'train']], \
-
vocab)
-
val_ds
= CaptioningData(
'val-images',
data[~
data[
'train']], \
-
vocab)
-
-
image, target, caption
= trn_ds.choose()
-
show(image, title
=caption, sz
=
5); print(target)
一个示例图像以及相应的标题和标记的单词索引如下:
5.为数据集创建数据加载器:
-
trn_dl
= DataLoader(trn_ds,
32, collate_fn
=trn_ds.collate_fn)
-
val_dl
= DataLoader(val_ds,
32, collate_fn
=val_ds.collate_fn)
-
inspect(
*
next(iter(trn_dl)), names
=
'images,targets,lengths')
样本批次将具有以下实体:
6.定义网络类:
- 定义编码器架构 – EncoderCNN:
-
from torch.nn.utils.rnn import pack_padded_
sequence
-
from torchvision import models
-
class EncoderCNN(nn.Module):
-
def __init__(
self, embed_
size):
-
""
"Load the pretrained ResNet-152 and replace
-
top fc layer."
""
-
super(EncoderCNN,
self).__init__()
-
resnet
= models.resnet
152(pretrained
=
True)
-
#
delete the
last fc layer.
-
modules
= list(resnet.children())[:-
1]
-
self.resnet
= nn.
Sequential(
*modules)
-
self.linear
= nn.Linear(resnet.fc.
in_features, \
-
embed_
size)
-
self.bn
= nn.BatchNorm
1d(embed_
size, \
-
momentum
=
0.01)
-
-
def forward(
self, images):
-
""
"Extract feature vectors from input images."
""
-
with torch.
no_grad():
-
features
=
self.resnet(images)
-
features
= features.reshape(features.
size(
0), -
1)
-
features
=
self.bn(
self.linear(features))
-
return features
在前面的代码中,我们正在获取预训练的 ResNet-152 模型,删除最后fc一层,将其连接到Linearsize 的层embed_size,然后将其通过批归一化 ( bn)。
- 获取encoder课程摘要:
-
encoder
= EncoderCNN(
256).
to(device)
-
!pip install torch_summary
-
from torchsummary import summary
-
print(summary(encoder,torch.
zeros(
32,3,224,224).
to(device)))
前面的代码给出以下输出:
- 定义解码器架构 – DecoderRNN:
-
class DecoderRNN(nn.Module):
-
def __init__(
self, embed_
size, hidden_
size, vocab_
size, \
-
num_layers, max_seq_
length
=
80):
-
""
"Set the hyper-parameters and build the layers."
""
-
super(DecoderRNN,
self).__init__()
-
self.embed
= nn.Embedding(vocab_
size, embed_
size)
-
self.lstm
= nn.LSTM(embed_
size, hidden_
size, \
-
num_layers, batch_
first
=
True)
-
self.linear
= nn.Linear(hidden_
size, vocab_
size)
-
self.max_seq_
length
= max_seq_
length
-
-
def forward(
self, features, captions, lengths):
-
""
"Decode image feature vectors and
-
generates captions."
""
-
embeddings
=
self.embed(captions)
-
embeddings
= torch.cat((features.unsqueeze(
1), \
-
embeddings),
1)
-
packed
= pack_padded_
sequence(embeddings, \
-
lengths.cpu(), batch_
first
=
True)
-
outputs, _
=
self.lstm(packed)
-
outputs
=
self.linear(outputs[
0])
-
return outputs
在前面的解码器中,让我们了解我们正在初始化的内容:
- self.embed:vocab x embed_size为每个单词创建和学习唯一嵌入的矩阵。
- self.lstm将CNNEncoder前一个时间步的词输出嵌入的输出作为输入,并返回每个时间步的隐藏状态。
- self.linear将每个隐藏状态转换为V我们将使用 softmax 的维向量,以获取时间步长的可能单词。
在该forward方法中,我们看到以下内容:
1.字幕(以整数形式发送)使用self.embed.
2.featuresfromEncoderCNN连接到embeddings。如果每个字幕的时间步数(下例中的L)为 80,则在串联后,时间步数将为 81。请参阅以下示例了解每个时间步中的馈送和预测内容:
3.使用pack_padded_sequences,连接的嵌入被打包到一个数据结构中,通过不在存在填充的时间步展开,让 RNN 计算更高效。直观的解释见下图:
- 在下图中,我们有三个句子,它们使用相应的词索引进行编码。一个词索引0代表填充索引。打包后,批量大小1在最后一个索引中,因为只有一个句子中的最后一个索引不是填充索引:
- 打包后的填充现在传递给 LSTM,如下所示:
代码中上一个插图的对应行是. 最后,LSTM 的输出通过线性层发送,因此维度数从 512 变为 vocab 大小。 outputs, _ = self.lstm(packed)
我们还将predict向 RNN 添加一个方法,该方法接受来自EncoderCNN每个特征的特征并返回每个特征的预期标记。我们将在训练后使用它来获取图像上的标题:
-
def predict(
self, features, states
=None):
-
""
"Generate captions for given image
-
features using greedy search."
""
-
sampled_ids
= []
-
inputs
= features.unsqueeze(
1)
-
for i
in range(
self.max_seq_
length):
-
hiddens, states
=
self.lstm(inputs, states)
-
# hiddens: (batch_
size,
1, hidden_
size)
-
outputs
=
self.linear(hiddens.squeeze(
1))
-
# outputs: (batch_
size, vocab_
size)
-
_, predicted
= outputs.m
ax(1)
-
# predicted: (batch_
size)
-
sampled_ids.append(predicted)
-
inputs
=
self.embed(predicted)
-
# inputs: (batch_
size, embed_
size)
-
inputs
= inputs.unsqueeze(
1)
-
# inputs: (batch_
size,
1, embed_
size)
-
-
sampled_ids
= torch.stack(sampled_ids,
1)
-
# sampled_ids: (batch_
size, max_seq_
length)
-
# convert predicted tokens
to strings
-
sentences
= []
-
for sampled_id
in sampled_ids:
-
sampled_id
= sampled_id.cpu().numpy()
-
sampled_caption
= []
-
for word_id
in sampled_id:
-
word
= vocab.itos[word_id]
-
sampled_caption.append(word)
-
if word
=
=
'<end>':
-
break
-
sentence
=
' '.join(sampled_caption)
-
sentences.append(
sentence)
-
return sentences
7.定义对一批数据进行训练的函数:
-
def train_batch(
data, encoder, decoder, optimizer, criterion):
-
encoder.train()
-
decoder.train()
-
images, captions, lengths
=
data
-
images
= images.
to(device)
-
captions
= captions.
to(device)
-
targets
= pack_padded_
sequence(captions, lengths.cpu(), \
-
batch_
first
=
True)[
0]
-
features
= encoder(images)
-
outputs
= decoder(features, captions, lengths)
-
loss
= criterion(outputs, targets)
-
decoder.
zero_grad()
-
encoder.
zero_grad()
-
loss.backward()
-
optimizer.step()
-
return loss
请注意,我们创建了一个targets由此调用的张量,其中将项目打包到一个向量中。正如您从上图中所知道的那样,pack_padded_sequence有助于以这样一种方式打包预测,以便更容易nn.CrossEntropyLoss使用打包target值调用输出。
8.定义对一批数据进行验证的函数:
-
@torch.
no_grad()
-
def
validate_batch(
data, encoder, decoder, criterion):
-
encoder.eval()
-
decoder.eval()
-
images, captions, lengths
=
data
-
images
= images.
to(device)
-
captions
= captions.
to(device)
-
targets
= pack_padded_
sequence(captions, lengths.cpu(), \
-
batch_
first
=
True)[
0]
-
features
= encoder(images)
-
outputs
= decoder(features, captions, lengths)
-
loss
= criterion(outputs, targets)
-
return loss
9.定义模型对象和损失函数,以及优化器:
-
encoder
= EncoderCNN(
256).
to(device)
-
decoder
= DecoderRNN(
256,
512, len(vocab.itos),
1).
to(device)
-
criterion
= nn.CrossEntropyLoss()
-
params
= list(decoder.parameters())
+ \
-
list(encoder.linear.parameters())
+ \
-
list(encoder.bn.parameters())
-
optimizer
= torch.optim.AdamW(params, lr
=
1e-
3)
-
n_epochs
=
10
-
log
=
Report(n_epochs)
10.在越来越多的时期训练模型:
-
for epoch
in range(n_epochs):
-
if epoch
=
=
5: optimizer
= torch.optim.AdamW(params, \
-
lr
=
1e-
4)
-
N
= len(trn_dl)
-
for i,
data
in enumerate(trn_dl):
-
trn_loss
= train_batch(
data, encoder, decoder, \
-
optimizer, criterion)
-
pos
= epoch
+ (
1
+i)
/N
-
log.
record(pos
=pos, trn_loss
=trn_loss,
end
=
'\r')
-
-
N
= len(val_dl)
-
for i,
data
in enumerate(val_dl):
-
val_loss
=
validate_batch(
data, encoder, decoder, \
-
criterion)
-
pos
= epoch
+ (
1
+i)
/N
-
log.
record(pos
=pos, val_loss
=val_loss,
end
=
'\r')
-
log.
report_avgs(epoch
+
1)
-
-
log.plot_epochs(log
=
True)
前面的代码生成训练和验证损失随时间增加而变化的输出:
11.定义一个给定图像生成预测的函数:
-
def load_image(image_path, transform
=None):
-
image
= Image.
open(image_path).convert(
'RGB')
-
image
= image.resize([
224,
224], Image.LANCZOS)
-
if transform
is
not None:
-
tfm_image
= transform(image)[None]
-
return image, tfm_image
-
-
def load_image_
and_predict(image_path):
-
transform
= transforms.Compose([
-
transforms.ToTensor(),
-
transforms.Normalize(\
-
(
0.485,
0.456,
0.406),
-
(
0.229,
0.224,
0.225))
-
])
-
org_image, tfm_image
= load_image(image_path, transform)
-
image_tensor
= tfm_image.
to(device)
-
encoder.eval()
-
decoder.eval()
-
feature
= encoder(image_tensor)
-
sentence
= decoder.predict(feature)[
0]
-
show(org_image, title
=
sentence)
-
return
sentence
-
-
files
= Glob(
'val-images')
-
load_image_
and_predict(choose(files))
前面给出了给定图像的预测:
从前面可以看出,给定图像(在前面的示例中显示为标题),我们可以生成合理的标题。
在本节中,我们学习了如何利用 CNN 和 RNN 一起生成字幕。在下一节中,我们将学习如何使用 CNN、RNN 和 CTC 损失函数来转录包含手写文字的图像。
转录手写图像
在上一节中,我们学习了如何从输入图像生成单词序列。在本节中,我们将学习如何以图像作为输入来生成字符序列。此外,我们将了解有助于转录手写图像的 CTC 损失函数。
在我们了解 CTC 损失函数之前,让我们了解为什么我们在图像字幕部分看到的架构可能不适用于手写转录的原因。与图像字幕不同,在图像中的内容和输出单词之间没有直接的相关性,在手写图像中,图像中出现的字符序列与输出序列之间存在直接相关性。因此,我们将遵循与上一节中设计的架构不同的架构。
另外,假设一个图像被分成20个部分的场景(假设一个图像中每个单词最多20个字符的场景),其中每个部分对应一个字符。一个人的笔迹可能会确保每个字符完全适合一个框,而另一个人的笔迹可能会混淆,使得每个框包含两个字符,而另一个人的笔迹可能会导致两个字符之间的间距太大以至于无法将一个单词放入 20时间步长(部分)。这就需要一种不同的方法来解决这个问题,即利用 CTC 损失函数——我们将在下一节中学习。
CTC损失的工作细节
想象一个场景,我们正在转录包含单词ab的图像。图像可能如下所示,并且输出始终为ab,无论我们选择以下三个图像中的哪一个:
在下一步中,我们将前面三个示例分为六个时间步,如下所示(其中每个框代表一个时间步):
现在,我们将预测每个时间步的输出字符——其中输出是词汇表中出现的单词概率的 softmax。假设我们正在执行 softmax,假设通过我们的模型(我们将在下一节中定义)运行图像后每个时间步的输出字符如下(每个单元的输出在图像上方提供) :
请注意,-表示在相应的时间步中不存在任何内容。此外,请注意字符b在两个不同的时间步中重复。
在最后一步中,我们将压缩输出(字符序列),该输出是通过将图像通过模型以将连续重复字符压缩为一个的方式获得的。
如果存在连续的相同字符预测,则压缩重复字符的输出的上述步骤导致最终输出如下:
-ab-
在另一种情况下,输出为abb,最终输出压缩后预计在两个b字符之间有分隔符,示例如下:
-abb-
现在我们了解了输入和输出值的外观的概念,在下一节中,让我们了解我们如何计算 CTC 损失值。
计算 CTC 损失值
对于我们在上一节中解决的问题,让我们考虑以下场景 - 下图中的圆圈中提供了在给定时间步中具有字符的概率(请注意,每个时间步的概率加起来为 1从t0到t5):
不过,为了让计算简单,为了让我们理解 CTC 损失值是如何计算的,让我们以图像只包含字符a而不包含单词ab的场景为例。此外,为了简化计算,我们假设只有三个时间步长:
如果每个时间步中的 softmax 类似于以下七个场景中的任何一个,我们就可以获得a的基本事实:
每个时间步的输出 | 概率。t 0中的字符 | 概率。t 1中的字符 | 概率。t 2中的字符 | 组合概率 | 最终概率 |
--a | 0.8 | 0.1 | 0.1 | 0.8×0.1×0.1 | 0.008 |
-aa | 0.8 | 0.9 | 0.1 | 0.8 x 0.9 x 0.1 | 0.072 |
aaa | 0.2 | 0.9 | 0.1 | 0.2×0.9×0.1 | 0.018 |
-a- | 0.8 | 0.9 | 0.8 | 0.8 x 0.9 x 0.8 | 0.576 |
-aa | 0.8 | 0.9 | 0.1 | 0.8 x 0.9 x 0.1 | 0.072 |
a-- | 0.2 | 0.1 | 0.8 | 0.2×0.1×0.8 | 0.016 |
aa- | 0.2 | 0.9 | 0.8 | 0.2×0.9×0.8 | 0.144 |
总体概率 | 0.906 |
从前面的结果可以看出,获得ground truth的总体概率为0.906。
0.094 的其余部分对应于结果未获得基本事实的概率。
让我们计算对应于所有可能的基本事实的总和的二进制交叉熵损失。
CTC 损失是导致基本事实 = -log(0.906) = 0.1 的组合的总体概率总和的负对数。
现在我们了解了 CTC 损失是如何计算的,让我们在下一节中构建用于从图像中进行手写转录的模型时实现这些知识。
代码中的手写转录
我们将采用以下策略来编写一个可以转录手写文字图像内容的网络:
- 导入图像数据集及其相应的转录。
- 给每个字符一个索引。
- 将图像通过卷积网络来获取与图像对应的特征图。
- 通过 RNN 传递特征图。
- 获取每个时间步的概率。
- 利用 CTC 损失函数来压缩输出并提供转录和相应的损失。
- 通过最小化 CTC 损失函数来优化网络的权重。
让我们在代码中执行前面的策略:
1.下载并导入图像数据集:
-
!wget https:
/
/www.dropbox.com
/s
/l
2ul
3upj
7dkv
4ou
/synthetic-data.zip
-
!unzip -qq synthetic-data.zip
在前面的代码中,我们下载了提供图像的数据集,并且图像的文件名包含与该图像对应的转录的基本事实。
下载的图像示例如下:
2.安装所需的包并导入它们:
!pip install torch_snippets torch_summary editdistance
- 导入包:
-
from torch_snippets
import *
-
from torchsummary
import summary
-
import editdistance
3.指定图像的位置和从图像中获取基本事实的函数:
-
device
=
'cuda'
if torch.cuda.
is_available()
else
'cpu'
-
fname
2label
= lambda fname: stem(fname).split(
'@')[
0]
-
images
= Glob(
'synthetic-data')
请注意,我们正在创建fname2label函数,因为图像的基本事实@在文件名中的符号之后可用。文件名示例如下:
4.定义字符的词汇表 ( vocab)、批量大小 ( B)、RNN 的时间步长 ( ) T、词汇表的长度 ( V)、高度 ( H) 和图像的宽度 ( W):
-
vocab
=
'QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm'
-
B,T,V
=
64,
32, len(vocab)
-
H,W
=
32,
128
5.定义OCRDataset数据集类:
- 定义__init__我们指定字符到字符 ID ( ) 的映射的方法,以及通过循环遍历charList( ) 的其他方式,以及时间步数 ( ) 和要获取的图像的文件路径 ( ) . 我们在这里使用and而不是使用's词汇,因为词汇更易于处理(包含较少数量的不同字符):invCharListvocabtimestepsitemscharListinvCharListtorchtextbuild
-
class OCRDataset(Dataset):
-
def __init__(
self, items, vocab
=vocab, \
-
preprocess_shape
=(H,W), timesteps
=T):
-
super().__init__()
-
self.items
= items
-
self.charList
= {ix
+
1:
ch
for ix,
ch \
-
in enumerate(vocab)}
-
self.charList.update({
0:
'`'})
-
self.invCharList
= {v:k
for k,v
in \
-
self.charList.items()}
-
self.ts
= timesteps
- 定义__len__ 和__getitem__方法:
-
def
__len__(
self):
-
return len(
self.items)
-
def
sample(
self):
-
return
self[randint(len(
self))]
-
def
__getitem__(
self, ix):
-
item =
self.items[ix]
-
image = cv2. imread(item,
0)
-
label = fname2label(item)
-
return image, label
请注意,在该__getitem__方法中,我们正在读取图像并使用fname2label我们之前定义的标签创建标签。
此外,我们正在定义一种sample方法,帮助我们从数据集中随机采样图像。
- 定义collate_fn方法,该方法获取一批图像并将它们及其标签附加到不同的列表中。此外,它将与图像对应的ground truth的字符转换为矢量格式(将每个字符转换为其对应的ID),最后存储标签长度和输入长度(始终是时间步数)每个图像。在计算损失值时,CTC 损失函数会利用标签长度和输入长度:
-
def
collate_fn(self, batch):
-
images, labels, label_lengths = [], [], []
-
label_vectors, input_lengths = [], []
-
for image, label in batch:
-
images.
append(torch.
Tensor(self.\
-
preprocess(image))[None,None])
-
label_lengths.
append(
len(label))
-
labels.
append(label)
-
label_vectors.
append(self.
str2vec(label))
-
input_lengths.
append(self.ts)
- 将前面的每个列表转换为 Torch 张量对象并返回images、labels、label_lengths、label_vectors和input_lengths:
-
images
= torch.cat(images).float().
to(device)
-
label_lengths
= torch.Tensor(label_lengths)\
-
.long().
to(device)
-
label_vectors
= torch.Tensor(label_vectors)\
-
.long().
to(device)
-
input_lengths
= torch.Tensor(
input_lengths)\
-
.long().
to(device)
-
return images, label_vectors, label_lengths, \
-
input_lengths, labels
- 定义str2vec函数,它将字符 ID 的输入转换为字符串:
-
def str
2vec(
self,
string, pad
=
True):
-
string
=
''.join([s
for s
in
string
if \
-
s
in
self.invCharList])
-
val
= list(map(lambda x:
self.invCharList[x], \
-
string))
-
if pad:
-
while len(val)
<
self.ts:
-
val.append(
0)
-
return val
在该str2vec函数中,我们从一串字符 ID 中获取字符,并0在标签的长度 ( len(val)) 小于时间步数( ) 的情况下附加带有填充索引的向量self.ts。
- 定义preprocess 函数,该函数接受图像 ( img)shape作为输入,将其处理为一致的 32 x 128 形状。请注意,除了调整图像大小之外,还需要进行额外的预处理,因为要在保持纵横比的同时调整图像大小。
定义图像的preprocess功能和目标形状,现在初始化为空白图像(白色图像 - target):
-
def preprocess(
self, img, shape
=(
32,128)):
-
target
= np.ones(shape)
*
255
获取图像的形状和预期形状:
-
try:
-
H, W
= shape
-
h, w
= img.shape
计算如何调整图像大小以保持纵横比:
-
fx
= H
/h
-
fy
= W
/w
-
f
= min(fx, fy)
-
_h
= int(h
*f)
-
_w
= int(w
*f)
调整图像大小并将其存储在前面定义的目标变量中:
-
_img
= cv
2.resize(img, (_w,_h))
-
target[:_h,:_w]
= _img
返回标准化图像(我们首先将图像转换为黑色背景,然后将像素缩放到 0 到 1 之间的值):
-
except:
-
...
-
return (
255-target)
/
255
- 定义将decoder_chars预测解码为单词的函数:
-
def decoder_chars(
self, pred):
-
decoded
=
""
-
last
=
""
-
pred
= pred.cpu().detach().numpy()
-
for i
in range(len(pred)):
-
k
= np.argmax(pred[i])
-
if k
>
0
and
self.charList[k] !
=
last:
-
last
=
self.charList[k]
-
decoded
= decoded
+
last
-
elif k
>
0
and
self.charList[k]
=
=
last:
-
continue
-
else:
-
last
=
""
-
return decoded.
replace(
" ",
" ")
在前面的代码中,我们一次循环遍历预测( ) ,pred获取置信度最高k的字符(如果上一个时间步中置信度最高的字符与当前时间步中置信度最高的字符不同,则将其last附加到字符上(相当于挤压,我们在 CTC 损失函数部分中讨论过) decoded.
- 定义计算字符和单词精度的方法:
-
def
wer(self, preds, labels):
-
c =
0
-
for p, l in
zip(preds, labels):
-
c += p.
lower().
strip() != l.
lower().
strip()
-
return
round(c/
len(preds),
4)
-
def
cer(self, preds, labels):
-
c, d = [], []
-
for p, l in
zip(preds, labels):
-
c.
append(editdistance.
eval(p, l) /
len(l))
-
return
round(np.
mean(c),
4)
- 定义一种在一组图像上评估模型并返回单词和字符错误率的方法:
-
def
evaluate(
self, model, ims, labels, lower
=
False):
-
model.eval()
-
preds
= model(ims).permute(
1,0,2) # B, T, V
+
1
-
preds
= [
self.decoder_chars(pred)
for pred
in preds]
-
return {
'char-error-rate':
self.cer(preds, labels), \
-
'word-error-rate':
self.wer(preds, labels), \
-
'char-accuracy':
1-
self.cer(preds, labels), \
-
'word-accuracy' :
1-
self.wer(preds, labels)}
6.在前面的代码中,我们对输入图像的通道进行了置换,以便按照模型的预期对数据进行预处理,使用decoder_chars函数对预测进行解码,然后返回字符错误率、单词错误率及其相应的准确度。
指定训练和验证数据集以及数据加载器:
-
from sklearn.model_selection import train_
test_split
-
trn_items,val_items
=train_
test_split(Glob(
'synthetic-data'), \
-
test_
size
=
0.2,
random_state
=
22)
-
trn_ds
= OCRDataset(trn_items)
-
val_ds
= OCRDataset(val_items)
-
-
trn_dl
= DataLoader(trn_ds, batch_
size
= B,\ collate_fn
-
=trn_ds.collate_fn,\
-
drop_
last
=
True,shuffle
=
True)
-
val_dl
=DataLoader(val_ds,batch_
size
=B,\
-
collate_fn
=val_ds.collate_fn,drop_
last
=
True)
7.构建网络架构:
- 构建 CNN 的基本块:
-
from torch_snippets import Reshape, Permute
-
class BasicBlock(nn.Module):
-
def __init__(
self, ni,
no, ks
=
3, st
=
1, \
-
padding
=
1, pool
=
2, drop
=
0.2):
-
super().__init__()
-
self.ks
= ks
-
self.
block
= nn.
Sequential(
-
nn.Conv
2d(ni,
no, kernel_
size
=ks, \
-
stride
=st, padding
=padding),
-
nn.BatchNorm
2d(
no, momentum
=
0.3),
-
nn.ReLU(inplace
=
True),
-
nn.MaxPool
2d(pool),
-
nn.Dropout
2d(drop)
-
)
-
def forward(
self, x):
-
return
self.
block(x)
- 构建神经网络类 OCR,其中 CNN 块和 RNN 块分别在和中的__init__方法中定义。接下来,我们定义层,它获取 RNN 的输出,并在通过密集层处理 RNN 输出后将其传递给 softmax 激活:self.modelself.rnnself.classification
-
class Ocr(nn.Module):
-
def __init__(
self, vocab):
-
super().__init__()
-
self.model
= nn.
Sequential(
-
BasicBlock(
1,
128),
-
BasicBlock(
128,
128),
-
BasicBlock(
128,
256, pool
=(
4,2)),
-
Reshape(-
1,
256,
32),
-
Permute(
2,
0,
1) # T, B, D
-
)
-
self.rnn
= nn.
Sequential(
-
nn.LSTM(
256,
256, num_layers
=
2, \
-
dropout
=
0.2, bidirectional
=
True),
-
)
-
self.classification
= nn.
Sequential(
-
nn.Linear(
512, vocab
+
1),
-
nn.LogSoftmax(-
1),
-
)
- 定义forward方法:
-
def forward(
self, x):
-
x
=
self.model(x)
-
x, lstm_states
=
self.rnn(x)
-
y
=
self.classification(x)
-
return y
-
在前面的代码中,我们在第一步中获取 CNN 输出,然后将其传递给 RNN 进行 fetchlstm_states和 RNN 输出x,最后将输出传递给分类层 ( self.classification) 并返回。
- 定义 CTC 损失函数:
-
def ctc(log_probs, target,
input_lengths, \ target_lengths
-
,
blank
=
0):
-
loss
= nn.CTCLoss(
blank
=
blank,
zero_infinity
=
True)
-
ctc_loss
= loss(log_probs, target, \
-
input_lengths, target_lengths)
-
return ctc_loss
在前面的代码中,我们利用了nn.CTCLoss方法来最小化ctc_loss,它采用置信矩阵,log_probs(每个时间步的预测),target(基本事实)input_lengths,以及target_lengths作为返回ctc_loss值的输入。
- 获取已定义模型的摘要:
-
model
= Ocr(len(vocab)).
to(device)
-
summary(model, torch.
zeros((
1,1,32,128)).
to(device))
前面的代码产生以下输出:
请注意,由于有 53 个字符的词汇表(26 x 2 = 52个字母和分隔符),因此输出有 53 个概率与批次中的每个图像相关联。
8.定义对一批数据进行训练的函数:
-
def train_batch(
data, model, optimizer, criterion):
-
model.train()
-
imgs, targets, label_lens,
input_lens, labels
=
data
-
optimizer.
zero_grad()
-
preds
= model(imgs)
-
loss
= criterion(preds, targets,
input_lens, label_lens)
-
loss.backward()
-
optimizer.step()
-
results
= trn_ds.
evaluate(model, imgs.
to(device),labels)
-
return loss, results
9.定义对一批数据进行验证的函数:
-
@torch.
no_grad()
-
def
validate_batch(
data, model):
-
model.eval()
-
imgs, targets, label_lens,
input_lens, labels
=
data
-
preds
= model(imgs)
-
loss
= criterion(preds, targets,
input_lens, label_lens)
-
return loss, val_ds.
evaluate(model, imgs.
to(device), \
-
labels)
10.定义模型对象、优化器、损失函数和 epoch 数:
-
model
= Ocr(len(vocab)).
to(device)
-
criterion
= ctc
-
-
optimizer
= optim.AdamW(model.parameters(), lr
=
3e-
3)
-
-
n_epochs
=
50
-
log
=
Report(n_epochs)
11.在越来越多的时期运行模型:
-
for ep
in range( n_epochs):
-
N
= len(trn_dl)
-
for ix,
data
in enumerate(trn_dl):
-
pos
= ep
+ (ix
+
1)
/N
-
loss, results
= train_batch(
data, model, optimizer, \
-
criterion)
-
ca, wa
= results[
'char-accuracy'], \
-
results[
'word-accuracy']
-
log.
record(pos
=pos, trn_loss
=loss, trn_char_acc
=ca, \
-
trn_word_acc
=wa,
end
=
'\r')
-
val_results
= []
-
N
= len(val_dl)
-
for ix,
data
in enumerate(val_dl):
-
pos
= ep
+ (ix
+
1)
/N
-
loss, results
=
validate_batch(
data, model)
-
ca, wa
= results[
'char-accuracy'], \
-
results[
'word-accuracy']
-
log.
record(pos
=pos, val_loss
=loss, val_char_acc
=ca, \
-
val_word_acc
=wa,
end
=
'\r')
-
-
log.
report_avgs(ep
+
1)
-
print()
-
for jx
in range(
5):
-
img, label
= val_ds.sample()
-
_img
=torch.Tensor(val_ds.preprocess(img)[None,None])\
-
.
to(device)
-
pred
= model(_img)[:,
0,:]
-
pred
= trn_ds.decoder_chars(pred)
-
print(f
'Pred: `{pred}` :: Truth: `{label}`')
-
print()
前面的代码产生以下输出:
从图中,我们可以看到该模型在验证数据集上的单词准确率约为 80%。
此外,训练结束时的预测如下:
到目前为止,我们已经了解了使用 CNN 和 RNN 的组合。在下一节中,我们将了解如何利用变压器架构来执行我们在前几章中处理的卡车与公共汽车数据集的对象检测。
使用 DETR 进行对象检测
在前面关于对象检测的章节中,我们了解了利用锚框/区域提议来执行对象分类和检测。但是,它涉及到提出对象检测的一系列步骤。DETR 是一种利用转换器提出端到端管道的技术,可大大简化对象检测网络架构。Transformer 是在 NLP 中执行各种任务的最流行和最新的技术之一。在本节中,我们将了解变压器 DETR 的工作细节,并对其进行编码以执行我们检测卡车与公共汽车的任务。
变压器的工作细节
Transformer 已被证明是解决序列到序列问题的卓越架构。截至撰写本书时,几乎所有 NLP 任务都具有来自 Transformer 的最先进的实现。此类网络仅使用线性层和 softmax 来创建自注意力(将在下一小节中详细解释)。自注意力有助于识别输入文本中单词之间的相互依赖关系。输入序列通常不超过 2,048 个项目,因为这对于文本应用程序来说已经足够大了。但是,如果图像要与转换器一起使用,则必须将它们展平,这会创建数千/数百万像素的序列(因为 300 x 300 x 3 图像将包含 270K 像素),这是不可行的。Facebook Research 提出了一种新的方法来绕过这个限制,方法是将特征图(其尺寸比输入图像更小)作为转换器的输入。让我们在这一节中了解变压器的基础知识,稍后了解相关的代码块。
变压器基础知识
Transformer 的核心是自注意力模块。它需要三个二维矩阵(称为查询( Q )、键( K ) 和值( V ) 矩阵)作为输入。矩阵可以具有非常大的嵌入大小(因为它们将包含文本大小 x 嵌入大小的值数量),因此在运行缩放点积之前,它们首先被分成更小的组件(下图中的步骤 1) -attention(下图中的步骤 2)。
让我们了解自注意力是如何工作的。在序列长度为 3 的假设场景中,我们将三个词嵌入(W 1、W 2和W 3)作为输入。假设每个嵌入的大小为 512。这些嵌入中的每一个都单独转换为三个附加向量,它们是与每个输入对应的查询、键和值向量:
由于每个向量的大小为 512,因此在它们之间进行矩阵乘法的计算成本很高。因此,我们将这些向量中的每一个分成八个部分,每个键、查询和值张量都有八组 (64 x 3) 向量,其中 64 是从 512(嵌入大小)/8(多头)中获得的3 是序列长度:
请注意,将有八组张量,,等等,因为有八个多头。
在每个部分中,我们首先在键和查询矩阵之间执行矩阵乘法。这样,我们最终得到一个 3 x 3 矩阵。通过 softmax 激活传递它。现在,我们有一个矩阵来显示每个单词相对于其他单词的重要性:
最后,我们将前面的张量输出与值张量进行矩阵相乘,得到我们的自注意力操作的输出:
然后我们结合这一步的八个输出,使用 concat 层返回(下图中的step3),最终得到一个大小为 512 x 3 的张量。由于 Q、K 和 V 矩阵的分裂,该层也称为多头自注意力(来源:https ://arxiv.org/pdf/1706.03762.pdf ):
如此复杂的网络背后的想法如下:
- 值( Vs ) 是在其键和查询矩阵的上下文中需要为给定输入学习的已处理嵌入。
- 查询( Qs ) 和键( Ks ) 以这样一种方式起作用,即它们的组合将创建正确的掩码,以便只有值矩阵的重要部分被馈送到下一层。
对于我们在计算机视觉中的示例,当搜索诸如马之类的对象时,查询应包含用于搜索尺寸较大且通常为棕色、黑色或白色的对象的信息。缩放点积注意力的 softmax 输出将反映图像中包含此颜色(棕色、黑色、白色等)的关键矩阵的那些部分。因此,自注意力层输出的值将包含图像中大致为所需颜色的部分,并出现在值矩阵中。
我们在网络中多次使用自注意力块,如下图所示。变压器网络包含一个编码网络(图的左侧),其输入是源序列。编码部分的输出用作解码部分的键和查询输入,而值输入将由神经网络独立于编码部分学习(来源:https ://arxiv.org/pdf/ 1706.03762.pdf):
最后,即使这是一个输入序列,也没有迹象表明哪个标记(单词)是第一个,哪个是下一个(因为线性层没有位置指示)。位置编码是可学习的嵌入(有时是硬编码向量),我们将其添加到每个输入中,作为其在序列中的位置的函数。这样做是为了让网络了解哪个词嵌入在序列中是第一个,哪个是第二个,依此类推。
在 PyTorch 中创建变压器网络的方法非常简单。您可以创建一个内置的转换器块,如下所示:
-
from torch
import nn
-
transformer = nn.
Transformer(hidden_dim, nheads, \
-
num_encoder_layers, num_decoder_layers)
这里,hidden_dim是嵌入的大小,nheads是多头自注意力中的头数,num_encoder_layers和分别num_decoder_layers是网络中编码和解码块的数量。
DETR的工作细节
普通变压器网络和 DETR 之间几乎没有关键区别。首先,我们的输入是图像,而不是序列。因此,DETR 将图像通过 ResNet 主干传递,得到一个大小为 256 的向量,然后可以将其视为一个序列。在我们的例子中,解码器的输入是对象查询嵌入,它们是在训练期间自动学习的。这些充当所有解码器层的查询矩阵。同样,对于每一层,关键矩阵和查询矩阵将是编码器块的最终输出矩阵,复制两次。变换器的最终输出将是Batch_Sizex 100 xEmbedding_Size张量,其中模型已经过训练100作为序列长度;也就是说,它学习了 100 个对象查询嵌入,并为每张图像返回 100 个向量,指示是否存在对象。这 100 个 xEmbedding_Size矩阵分别馈送到对象分类模块和对象回归模块,它们分别独立预测是否存在对象(以及它是什么)以及边界框坐标是什么。这两个模块都是简单的层。 nn.Linear
在高层次上,DETR 的架构如下(来源:https ://arxiv.org/pdf/2005.12872.pdf ):
DETR 的较小变体之一的定义如下:
- 创建 DETR 模型类:
-
from collections import OrderedDict
-
class DETR(nn.Module):
-
def __init__(
self,num_classes,hidden_dim
=
256,nheads
=
8, \
-
num_encoder_layers
=
6, num_decoder_layers
=
6):
-
super().__init__()
-
self.backbone
= resnet
50()
-
我们将只从 ResNet 中提取几层并丢弃其余层。这几层包含以下列表中给出的名称:
-
layers
= OrderedDict()
-
for name,module
in
self.backbone.named_modules():
-
if name
in [
'conv1',
'bn1',
'relu',
'maxpool', \
-
'layer1',
'layer2',
'layer3' ,
'layer4']:
-
layers[name]
= module
-
self.backbone
= nn.
Sequential(layers)
-
self.conv
= nn.Conv
2d(
2048, hidden_dim,
1)
-
self.transformer
= nn.Transformer(\
-
hidden_dim, nheads, \
-
num_encoder_layers, \
-
num_decoder_layers)
-
self.linear_
class
= nn.Linear(hidden_dim, \
-
num_classes
+
1)
-
self.linear_bbox
= nn.Linear(hidden_dim,
4)
在前面的代码中,我们指定了以下内容:
- 按顺序排列的感兴趣层 ( self.backbone)
- 卷积运算 ( self.conv)
- 变压器块 ( self.transformer)
- 最终连接获得的类数(self.linear_class)
- 边界框 ( self.linear_box)
- 定义编码器和解码器层的位置嵌入:
-
self.query_pos
= nn.Parameter(torch.rand(
100, \
-
hidden_dim))
-
self.row_embed
= nn.Parameter(torch.rand(
50, \
-
hidden_dim
/
/
2))
-
self.
col_embed
= nn.Parameter(torch.rand(
50, \
-
hidden_dim
/
/
2))
self.query_pos是解码器层的位置嵌入输入,而self.row_embed和self.col_embed形成编码器层的二维位置嵌入。
- 定义forward方法:
-
def forward(
self, inputs):
-
x
=
self.backbone(inputs)
-
h
=
self.conv(x)
-
H, W
= h.shape[-
2:]
-
''
'Below operation is rearranging the positional
-
embedding vectors for encoding layer'
''
-
pos
= torch.cat([\
-
self.
col_embed[:W].unsqueeze(
0).repeat(H,
1,
1),\
-
self.row_embed[:H].unsqueeze(
1).repeat(
1, W,
1),\
-
], dim
=-
1).flatten(
0,
1).unsqueeze(
1)
-
''
'Finally, predict on the feature map obtained
-
from resnet using the transformer network'
''
-
h
=
self.transformer(pos
+
0.1
*h.flatten(
2)\
-
.permute(
2,
0,
1), \
-
self.query_pos.unsqueeze(
1))\
-
.transpose(
0,
1)
-
''
'post process the output `h` to obtain class
-
probability and bounding boxes'
''
-
return {
'pred_logits':
self.linear_
class(h), \
-
'pred_boxes':
self.linear_bbox(h).sigmoid()}
您可以加载在 COCO 数据集上训练的预训练模型,并将其用于预测通用类。下一节将解释预测逻辑,您也可以在此模型上使用相同的函数(当然,使用 COCO 类):
-
detr
= DETR(num_classes
=
91)
-
state_dict
= torch.hub.load_state_dict_
from_url(url
=\
'https://dl.fbaipublicfiles.com/detr/detr_demo-da2a99e9.pth'\
-
,map_
location
=
'cpu', check_hash
=
True)
-
detr.load_state_dict(state_dict)
-
detr.eval();
请注意,与我们在第 7 章“对象检测基础”和第 8 章“高级对象检测”中学习的其他对象检测技术相比,DETR 可以一次性获取预测。
更详细的 DETR 架构版本如下(来源:https ://arxiv.org/pdf/2005.12872.pdf ):
在主干段中,我们正在获取图像特征,然后将其传递给编码器,该编码器将图像特征与位置嵌入连接起来。
self.row_embed, self.col_embed本质上,该方法中的位置嵌入__init__有助于编码有关图像中各种对象位置的信息。编码器采用位置嵌入和图像特征的连接来获得隐藏状态向量h(在前向方法中),该向量作为输入传递给解码器。该转换器输出进一步馈送到两个线性网络,一个用于对象识别,一个用于边界框回归。变压器的所有复杂性都隐藏在self.transformer网络模块中。
训练使用了一种新的匈牙利损失,它负责将对象识别为一个集合并惩罚冗余预测。这完全消除了对非最大抑制的需要。匈牙利损失的细节超出了本书的范围,我们鼓励你仔细阅读原始论文中的工作细节。
解码器采用编码器隐藏状态向量和对象查询的组合。对象查询以与位置嵌入/锚框类似的方式工作,以得出五个预测——一个用于对象的类别,另外四个用于与对象对应的边界框。
凭借对 DETR 工作细节的直觉和高级理解,让我们在下一节中对其进行编码。
在代码中使用transformers进行检测
在下面的代码中,我们将编写 DETR 来预测我们感兴趣的对象——公共汽车与卡车:
1.导入数据集并创建一个名为 的文件夹detr:
-
import os
-
if
not os.path.exists(
'open-images-bus-trucks'):
-
!pip install -q torch_snippets torchsummary
-
!wget --quiet https:
/
/www.dropbox.com
/s
/agmzwk
95v
96ihic
/open-images-bus-trucks.tar.xz
-
!tar -xf open-images-bus-trucks.tar.xz
-
!rm open-images-bus-trucks.tar.xz
-
!git clone https:
/
/github.com
/sizhky
/detr
/
-
%cd detr
- 将注释图像移动到detr文件夹:
-
%cd ..
/open-images-bus-trucks
/annotations
-
!cp mini_
open_images_train_coco_
format.json\
-
instances_train
2017.json
-
!cp mini_
open_images_val_coco_
format.json\
-
instances_val
2017.json
-
%cd ..
-
!ln -s images
/ train
2017
-
!ln -s images
/ val
2017
-
%cd ..
/detr
- 定义感兴趣的类:
CLASSES = ['', 'BUS','TRUCK']
2.导入预训练的 DETR 模型:
-
from torch_snippets import
*
-
if
not os.path.exists(
'detr-r50-e632da11.pth'):
-
!wget https:
/
/dl.fbaipublicfiles.com
/detr
/detr-r
50-e
632da
11.pth
-
checkpoint
= torch.load(
"detr-r50-e632da11.pth", \
-
map_
location
=
'cpu')
-
del checkpoint[
"model"][
"class_embed.weight"]
-
del checkpoint[
"model"][
"class_embed.bias"]
-
torch.save(checkpoint
"detr-r50_no-class-head.pth")
3.open-images-bus-trucks使用文件夹中的图像和注释训练模型:
-
!python main.py --coco_path ..
/open-images-bus-trucks
/\
-
--epochs
10 --lr
=
1e-
4 --batch_
size
=
2 --num_workers
=
4\
-
--
output_dir
=
"outputs" --
resume
=
"detr-r50_no-class-head.pth"
4.一旦我们训练模型,从文件夹中加载它:
-
from main import
get_args_parser, argparse, build_model
-
parser
=argparse.ArgumentParser(
'DETR training and \
-
evaluation script', parents
=[
get_args_parser()])
-
args, _
= parser.parse_known_args()
-
-
model, _, _
= build_model(args)
-
model.load_state_dict(torch.load(
"outputs/checkpoint.pth")\
-
[
'model']);
5.后处理预测以获取图像和对象周围的边界框:
-
from PIL import Image, ImageDraw, ImageFont
-
-
#
standard PyTorch mean-std
input image normalization
-
# colors
for visualization
-
COLORS
= [[
0.000,
0.447,
0.741], [
0.850,
0.325,
0.098],
-
[
0.929,
0.694,
0.125], [
0.494,
0.184,
0.556],
-
[
0.466,
0.674,
0.188], [
0.301,
0.745,
0.933]]
-
-
transform
= T.Compose([
-
T.Resize(
800),
-
T.ToTensor(),
-