fast.ai 深度学习笔记(四)(1)https://developer.aliyun.com/article/1482712
深度学习 2:第 2 部分第 9 课
原文:
medium.com/@hiromi_suenaga/deep-learning-2-part-2-lesson-9-5f0cf9e4bb5b
译者:飞龙
来自 fast.ai 课程的个人笔记。随着我继续复习课程以“真正”理解它,这些笔记将继续更新和改进。非常感谢 Jeremy 和Rachel 给了我这个学习的机会。
链接
回顾
上周的内容:
- Pathlib;JSON
- 字典推导
- Defaultdict
- 如何在 fastai 源代码中跳转
- matplotlib OO API
- Lambda 函数
- 边界框坐标
- 自定义头部;边界框回归
来自第 1 部分:
- 如何查看 DataLoader 中的模型输入
- 如何查看模型输出
数据增强和边界框[2:58]
fastai 的尴尬问题:
分类器是任何具有分类或二元因变量的东西。与回归相对,回归是任何具有连续因变量的东西。命名有点混乱,但将在未来得到解决。在这里,continuous
是True
,因为我们的因变量是边界框的坐标 — 因此这实际上是一个回归器数据。
tfms = tfms_from_model(f_model, sz, crop_type=CropType.NO, aug_tfms=augs) md = ImageClassifierData.from_csv(PATH, JPEGS, BB_CSV, tfms=tfms, **continuous=True**, bs=4)
让我们创建一些数据增强[4:40]
augs = [RandomFlip(), RandomRotate(30), RandomLighting(0.1,0.1)]
通常,我们使用 Jeremy 为我们创建的这些快捷方式,但它们只是随机增强的列表。但您可以轻松创建自己的(大多数,如果不是全部,都以“Random”开头)。
tfms = tfms_from_model( f_model, sz, crop_type=CropType.NO, aug_tfms=augs ) md = ImageClassifierData.from_csv( PATH, JPEGS, BB_CSV, tfms=tfms, continuous=True, bs=4 ) idx=3 fig,axes = plt.subplots(3,3, figsize=(9,9)) for i,ax in enumerate(axes.flat): x,y=next(iter(md.aug_dl)) ima=md.val_ds.denorm(to_np(x))[idx] b = bb_hw(to_np(y[idx])) print(b) show_img(ima, ax=ax) draw_rect(ax, b) ''' [115\. 63\. 240\. 311.] [ 115\. 63\. 240\. 311.] [ 115\. 63\. 240\. 311.] [ 115\. 63\. 240\. 311.] [ 115\. 63\. 240\. 311.] [ 115\. 63\. 240\. 311.] [ 115\. 63\. 240\. 311.] [ 115\. 63\. 240\. 311.] [ 115\. 63\. 240\. 311.]*
如您所见,图像会旋转并且光照会变化,但边界框不会移动,而且位置不正确[6:17]。这是数据增强的问题,当您的因变量是像素值或以某种方式与自变量相关联时,它们需要一起增强。如您在边界框坐标[ 115. 63. 240. 311.]
中所看到的,我们的图像是 224 乘以 224 — 因此它既没有缩放也没有裁剪。因变量需要经历所有几何变换,就像自变量一样。
要执行此操作[7:10],每个转换都有一个可选的tfm_y
参数:
augs = [ RandomFlip(tfm_y=TfmType.COORD), RandomRotate(30, tfm_y=TfmType.COORD), RandomLighting(0.1,0.1, tfm_y=TfmType.COORD) ] tfms = tfms_from_model( f_model, sz, crop_type=CropType.NO, tfm_y=TfmType.COORD, aug_tfms=augs ) md = ImageClassifierData.from_csv( PATH, JPEGS, BB_CSV, tfms=tfms, continuous=True, bs=4 )
TrmType.COORD
表示y值表示坐标。这需要添加到所有增强以及tfms_from_model
中,后者负责裁剪、缩放、调整大小、填充等。
idx=3 fig,axes = plt.subplots(3,3, figsize=(9,9)) for i,ax in enumerate(axes.flat): x,y=next(iter(md.aug_dl)) ima=md.val_ds.denorm(to_np(x))[idx] b = bb_hw(to_np(y[idx])) print(b) show_img(ima, ax=ax) draw_rect(ax, b) ''' [ 48\. 34\. 112\. 188.] [ 65\. 36\. 107\. 185.] [ 49\. 27\. 131\. 195.] [ 24\. 18\. 147\. 204.] [ 61\. 34\. 113\. 188.] [ 55\. 31\. 121\. 191.] [ 52\. 19\. 144\. 203.] [ 7\. 0\. 193\. 222.] [ 52\. 38\. 105\. 182.]*
现在,边界框随图像移动并位于正确位置。您可能会注意到有时看起来像底部行中间的那个奇怪。这是我们拥有的信息的限制。如果对象占据原始边界框的角落,那么在图像旋转后,您的新边界框需要更大。因此,您必须小心不要对边界框进行过高的旋转,因为没有足够的信息使它们保持准确。如果我们正在进行多边形或分割,我们将不会遇到这个问题。
这就是为什么框变大了
tfm_y = TfmType.COORD augs = [ RandomFlip(tfm_y=tfm_y), RandomRotate(3, **p=0.5**, tfm_y=tfm_y), RandomLighting(0.05,0.05, tfm_y=tfm_y) ] tfms = tfms_from_model( f_model, sz, crop_type=CropType.NO, tfm_y=tfm_y, aug_tfms=augs ) md = ImageClassifierData.from_csv( PATH, JPEGS, BB_CSV, tfms=tfms, continuous=True )
因此,在这里,我们最多进行 3 度旋转,以避免这个问题[9:14]。它也只有一半的时间旋转(p=0.5
)。
custom_head[9:34]
learn.summary()
将通过模型运行一小批数据,并打印出每一层张量的大小。正如您所看到的,在Flatten
层之前,张量的形状为 512 乘以 7 乘以 7。因此,如果它是一个秩为 1 的张量(即一个单一向量),其长度将为 25088(512 * 7 * 7),这就是为什么我们自定义标题的输入大小为 25088。输出大小为 4,因为它是边界框坐标。
head_reg4 = nn.Sequential(Flatten(), nn.Linear(25088,4)) learn = ConvLearner.pretrained(f_model, md, custom_head=head_reg4) learn.opt_fn = optim.Adam learn.crit = nn.L1Loss()
单个对象检测[10:35]
让我们将这两者结合起来,创建一个可以对每个图像中最大的对象进行分类和定位的东西。
训练神经网络有 3 件事情我们需要做:
- 数据
- 架构
- 损失函数
1. 提供数据
我们需要一个ModelData
对象,其独立变量是图像,依赖变量是一个包含边界框坐标和类别标签的元组。有几种方法可以做到这一点,但这里是 Jeremy 想出的一个特别懒惰和方便的方法,即创建两个代表我们想要的两个不同依赖变量的ModelData
对象(一个带有边界框坐标,一个带有类别)。
f_model=resnet34 sz=224 bs=64 val_idxs = get_cv_idxs(len(trn_fns)) tfms = tfms_from_model( f_model, sz, crop_type=CropType.NO, tfm_y=TfmType.COORD, aug_tfms=augs ) md = ImageClassifierData.from_csv( PATH, JPEGS, BB_CSV, tfms=tfms, continuous=True, val_idxs=val_idxs ) md2 = ImageClassifierData.from_csv( PATH, JPEGS, CSV, tfms=tfms_from_model(f_model, sz) )
数据集可以是任何具有__len__
和__getitem__
的东西。这里有一个数据集,它向现有数据集添加了第二个标签:
class ConcatLblDataset(Dataset): def __init__(self, ds, y2): self.ds,self.y2 = ds,y2 def __len__(self): return len(self.ds) def __getitem__(self, i): x,y = self.ds[i] return (x, (y,self.y2[i]))
ds
:包含独立和依赖变量y2
:包含额外的依赖变量(x, (y,self.y2[i]))
:__getitem___
返回一个独立变量和两个依赖变量的组合。
我们将用它来将类别添加到边界框标签中。
trn_ds2 = ConcatLblDataset(md.trn_ds, md2.trn_y) val_ds2 = ConcatLblDataset(md.val_ds, md2.val_y)
这是一个例子的依赖变量:
val_ds2[0][1]*(array([ 0., 49., 205., 180.], dtype=float32), 14)*
我们可以用这些新的数据集替换数据加载器的数据集。
md.trn_dl.dataset = trn_ds2 md.val_dl.dataset = val_ds2
我们必须在绘图之前从数据加载器中对图像进行denorm
alize。
x,y = next(iter(md.val_dl)) idx = 3 ima = md.val_ds.ds.denorm(to_np(x))[idx] b = bb_hw(to_np(y[0][idx])); b ''' array([ 52., 38., 106., 184.], dtype=float32) ''' ax = show_img(ima) draw_rect(ax, b) draw_text(ax, b[:2], md2.classes[y[1][idx]])
2. 选择架构[13:54]
架构将与我们用于分类器和边界框回归的相同,但我们将它们结合起来。换句话说,如果我们有c
个类别,那么最终层中所需的激活数量是 4 加上c
。4 用于边界框坐标和c
个概率(每个类别一个)。
这次我们将使用额外的线性层,再加上一些 dropout,来帮助我们训练一个更灵活的模型。一般来说,如果预训练的主干适合,我们希望我们的自定义头部能够独立解决问题。因此,在这种情况下,我们尝试做了很多事情——分类器和边界框回归,所以单个线性层似乎不够。如果你想知道为什么第一个ReLU
后面没有BatchNorm1d
,那是因为 ResNet 主干已经有BatchNorm1d
作为最后一层。
head_reg4 = nn.Sequential( Flatten(), nn.ReLU(), nn.Dropout(0.5), nn.Linear(25088,256), nn.ReLU(), nn.BatchNorm1d(256), nn.Dropout(0.5), nn.Linear(256, 4+len(cats)), ) models = ConvnetBuilder(f_model, 0, 0, 0, custom_head=head_reg4) learn = ConvLearner(md, models) learn.opt_fn = optim.Adam
3. 损失函数[15:46]
损失函数需要查看这些4 + len(cats)
激活,并决定它们是否良好——这些数字是否准确反映了图像中最大对象的位置和类别。我们知道如何做到这一点。对于前 4 个激活,我们将像以前一样使用 L1Loss(L1Loss 类似于均方误差——它使用绝对值的和,而不是平方误差的和)。对于其余的激活,我们可以使用交叉熵损失。
def detn_loss(input, target): bb_t,c_t = target bb_i,c_i = input[:, :4], input[:, 4:] bb_i = F.sigmoid(bb_i)*224 # I looked at these quantities separately first then picked a # multiplier to make them approximately equal return F.l1_loss(bb_i, bb_t) + F.cross_entropy(c_i, c_t)*20 def detn_l1(input, target): bb_t,_ = target bb_i = input[:, :4] bb_i = F.sigmoid(bb_i)*224 return F.l1_loss(V(bb_i),V(bb_t)).data def detn_acc(input, target): _,c_t = target c_i = input[:, 4:] return accuracy(c_i, c_t) learn.crit = detn_loss learn.metrics = [detn_acc, detn_l1]
input
:激活target
:真实值bb_t,c_t = target
:我们的自定义数据集返回一个包含边界框坐标和类别的元组。这个赋值将对它们进行解构。bb_i,c_i = input[:, :4], input[:, 4:]
:第一个:
是用于批处理维度。b_i = F.sigmoid(bb_i)*224
:我们知道我们的图像是 224x224。Sigmoid
将强制它在 0 和 1 之间,并将其乘以 224,以帮助我们的神经网络处于必须的范围内。
**问题:**一般规则是,在 ReLU 之前还是之后放置 BatchNorm 更好[18:02]?Jeremy 建议在 ReLU 之后放置 BatchNorm,因为 BatchNorm 旨在朝着零均值一标准差移动。因此,如果你在它之后放置 ReLU,你就在零处截断它,所以没有办法创建负数。但如果你先放 ReLU 再放 BatchNorm,它确实有这个能力,并且会给出稍微更好的结果。话虽如此,无论哪种方式都不是太大的问题。你会在这门课程的这部分看到,大多数时候 Jeremy 会先 ReLU 再 BatchNorm,但有时会相反,当他想要与论文保持一致时。
问题:在 BatchNorm 之后使用 dropout 的直觉是什么?BatchNorm 不是已经很好地进行了正则化吗[19:12]?BatchNorm 做正则化的效果还可以,但是如果回想第 1 部分,我们讨论过避免过拟合的一系列方法,添加 BatchNorm 是其中之一,数据增强也是其中之一。但是仍然有可能过拟合。关于 dropout 的一个好处是它有一个参数来指定要丢弃多少。参数非常好,特别是决定要进行多少正则化,因为它让你可以构建一个很大的超参数化模型,然后决定要进行多少正则化。Jeremy 倾向于总是从p=0
开始添加 dropout,然后随着添加正则化,他可以只需更改 dropout 参数,而不必担心是否保存了一个模型,他希望能够重新加载它,但如果一个中有 dropout 层而另一个中没有,它将无法加载。这样,保持一致性。
现在我们有了输入和目标,我们可以计算 L1 损失并添加交叉熵[20:39]:
F.l1_loss(bb_i, bb_t) + F.cross_entropy(c_i, c_t)*20
这是我们的损失函数。交叉熵和 L1 损失可能处于非常不同的尺度——在这种情况下,较大的那个将占主导地位。在这种情况下,Jeremy 打印出值并发现如果我们将交叉熵乘以 20,它们就会大致处于相同的尺度。
lr=1e-2 learn.fit(lr, 1, cycle_len=3, use_clr=(32,5)) ''' epoch trn_loss val_loss detn_acc detn_l1 0 72.036466 45.186367 0.802133 32.647586 1 51.037587 36.34964 0.828425 25.389733 2 41.4235 35.292709 0.835637 24.343577 [35.292709, 0.83563701808452606, 24.343576669692993] '''
在训练时打印信息是很好的,所以我们抓取了 L1 损失并将其添加为指标。
learn.save('reg1_0') learn.freeze_to(-2) lrs = np.array([lr/100, lr/10, lr]) learn.fit(lrs/5, 1, cycle_len=5, use_clr=(32,10)) ''' epoch trn_loss val_loss detn_acc detn_l1 0 34.448113 35.972973 0.801683 22.918499 1 28.889909 33.010857 0.830379 21.689888 2 24.237017 30.977512 0.81881 20.817996 3 21.132993 30.60677 0.83143 20.138552 4 18.622983 30.54178 0.825571 19.832196 [30.54178, 0.82557091116905212, 19.832195997238159] ''' learn.unfreeze() learn.fit(lrs/10, 1, cycle_len=10, use_clr=(32,10)) ''' epoch trn_loss val_loss detn_acc detn_l1 0 15.957164 31.111507 0.811448 19.970753 1 15.955259 32.597153 0.81235 20.111022 2 15.648723 32.231941 0.804087 19.522853 3 14.876172 30.93821 0.815805 19.226574 4 14.113872 31.03952 0.808594 19.155093 5 13.293885 29.736671 0.826022 18.761728 6 12.562566 30.000023 0.827524 18.82006 7 11.885125 30.28841 0.82512 18.904158 8 11.498326 30.070133 0.819712 18.635296 9 11.015841 30.213772 0.815805 18.551489 [30.213772, 0.81580528616905212, 18.551488876342773] '''
检测准确率在 80%左右,与之前相同。这并不令人惊讶,因为 ResNet 是设计用于分类的,所以我们不会指望能够以这种简单的方式改进事情。它确实不是设计用于边界框回归的。实际上,它是明确设计成不关心几何形状的——它取最后的 7x7 激活网格并将它们全部平均在一起,丢弃了所有关于每个位置的信息。
有趣的是,当我们同时进行准确性(分类)和边界框时,L1 似乎比我们只进行边界框回归时要好一点[22:46]。如果这对你来说是违反直觉的,那么这将是本课后需要考虑的主要问题之一,因为这是一个非常重要的想法。这个想法是——找出图像中的主要对象是比较困难的部分。然后确定边界框的确切位置和类别是一种简单的方式。因此,当你有一个同时指出对象是什么和对象在哪里的单个网络时,它将共享所有关于找到对象的计算。所有这些共享的计算非常高效。当我们反向传播类别和位置的错误时,所有这些信息都将帮助计算找到最大对象的周围。因此,每当你有多个任务共享某些概念,这些任务需要完成它们的工作,它们很可能应该至少共享网络的一些层。今天晚些时候,我们将看一个模型,其中大部分层都是共享的,除了最后一层。
以下是结果[24:34]。与以前一样,在图像中有单个主要对象时表现良好。
多标签分类[25:29]
我们希望继续构建比上一个模型稍微复杂的模型,这样如果某些东西停止工作,我们就知道出了什么问题。以下是上一个笔记本中的函数:
%matplotlib inline %reload_ext autoreload %autoreload 2 from fastai.conv_learner import * from fastai.dataset import * import json, pdb from PIL import ImageDraw, ImageFont from matplotlib import patches, patheffects torch.backends.cudnn.benchmark=True
设置
PATH = Path('data/pascal') trn_j = json.load((PATH / 'pascal_train2007.json').open()) IMAGES,ANNOTATIONS,CATEGORIES = [ 'images', 'annotations', 'categories' ] FILE_NAME,ID,IMG_ID,CAT_ID,BBOX = \ 'file_name','id','image_id', 'category_id','bbox' cats = dict((o[ID], o['name']) for o in trn_j[CATEGORIES]) trn_fns = dict((o[ID], o[FILE_NAME]) for o in trn_j[IMAGES]) trn_ids = [o[ID] for o in trn_j[IMAGES]] JPEGS = 'VOCdevkit/VOC2007/JPEGImages' IMG_PATH = PATH/JPEGSdef get_trn_anno(): trn_anno = collections.defaultdict(lambda:[]) for o in trn_j[ANNOTATIONS]: if not o['ignore']: bb = o[BBOX] bb = np.array([ bb[1], bb[0], bb[3]+bb[1]-1, bb[2]+bb[0]-1 ]) trn_anno[o[IMG_ID]].append((bb,o[CAT_ID])) return trn_anno trn_anno = get_trn_anno() def show_img(im, figsize=None, ax=None): if not ax: fig,ax = plt.subplots(figsize=figsize) ax.imshow(im) ax.set_xticks(np.linspace(0, 224, 8)) ax.set_yticks(np.linspace(0, 224, 8)) ax.grid() ax.set_yticklabels([]) ax.set_xticklabels([]) return ax def draw_outline(o, lw): o.set_path_effects([ patheffects.Stroke(linewidth=lw, foreground='black'), patheffects.Normal() ]) def draw_rect(ax, b, color='white'): patch = ax.add_patch(patches.Rectangle( b[:2], *b[-2:], fill=False, edgecolor=color, lw=2 )) draw_outline(patch, 4) def draw_text(ax, xy, txt, sz=14, color='white'): text = ax.text( *xy, txt, verticalalignment='top', color=color, fontsize=sz, weight='bold' ) draw_outline(text, 1) def bb_hw(a): return np.array([a[1],a[0],a[3]-a[1],a[2]-a[0]]) def draw_im(im, ann): ax = show_img(im, figsize=(16,8)) for b,c in ann: b = bb_hw(b) draw_rect(ax, b) draw_text(ax, b[:2], cats[c], sz=16) def draw_idx(i): im_a = trn_anno[i] im = open_image(IMG_PATH/trn_fns[i]) draw_im(im, im_a) • 75
多类别[26:12]
MC_CSV = PATH/'tmp/mc.csv' trn_anno[12] ''' [(array([ 96, 155, 269, 350]), 7)] ''' mc = [set([cats[p[1]] for p in trn_anno[o]]) for o in trn_ids] mcs = [' '.join(str(p) for p in o) for o in mc] df = pd.DataFrame({ 'fn': [trn_fns[o] for o in trn_ids], 'clas': mcs }, columns=['fn','clas']) df.to_csv(MC_CSV, index=False)
有一个学生指出,通过使用 Pandas,我们可以比使用collections.defaultdict
更简单地完成一些事情,并分享了这个gist。您越了解 Pandas,就越会意识到它是解决许多不同问题的好方法。
问题:当您在较小的模型基础上逐步构建时,您是否重复使用它们作为预训练权重?还是将其丢弃然后从头开始重新训练?当 Jeremy 像这样逐步弄清楚事情时,他通常倾向于丢弃,因为重用预训练权重会引入不必要的复杂性。但是,如果他试图达到一个可以在非常大的图像上训练的点,他通常会从更小的模型开始,并经常重用这些权重。
f_model=resnet34 sz=224 bs=64 tfms = tfms_from_model(f_model, sz, crop_type=CropType.NO) md = ImageClassifierData.from_csv(PATH, JPEGS, MC_CSV, tfms=tfms) learn = ConvLearner.pretrained(f_model, md) learn.opt_fn = optim.Adamlr = 2e-2 learn.fit(lr, 1, cycle_len=3, use_clr=(32,5)) ''' epoch trn_loss val_loss <lambda> 0 0.104836 0.085015 0.972356 1 0.088193 0.079739 0.972461 2 0.072346 0.077259 0.974114 [0.077258907, 0.9741135761141777] ''' lrs = np.array([lr/100, lr/10, lr]) learn.freeze_to(-2)learn.fit(lrs/10, 1, cycle_len=5, use_clr=(32,5)) ''' epoch trn_loss val_loss <lambda> 0 0.063236 0.088847 0.970681 1 0.049675 0.079885 0.973723 2 0.03693 0.076906 0.975601 3 0.026645 0.075304 0.976187 4 0.018805 0.074934 0.975165 [0.074934497, 0.97516526281833649] ''' learn.save('mclas') learn.load('mclas') y = learn.predict() x,_ = next(iter(md.val_dl)) x = to_np(x) fig, axes = plt.subplots(3, 4, figsize=(12, 8)) for i,ax in enumerate(axes.flat): ima=md.val_ds.denorm(x)[i] ya = np.nonzero(y[i]>0.4)[0] b = '\n'.join(md.classes[o] for o in ya) ax = show_img(ima, ax=ax) draw_text(ax, (0,0), b) plt.tight_layout()
多类别分类非常直接。在这一行中使用set
的一个小调整,以便每种对象类型只出现一次。
mc = [set([cats[p[1]] for p in trn_anno[o]]) for o in trn_ids]
SSD 和 YOLO
我们有一个输入图像通过卷积网络,输出大小为4+c
的向量,其中c=len(cats)
。这为我们提供了一个用于单个最大对象的对象检测器。现在让我们创建一个可以找到 16 个对象的检测器。显而易见的方法是取最后一个线性层,而不是有4+c
个输出,我们可以有16x(4+c)
个输出。这为我们提供了 16 组类别概率和 16 组边界框坐标。然后我们只需要一个损失函数,检查这 16 组边界框是否正确表示了图像中的最多 16 个对象(我们将在后面讨论损失函数)。
第二种方法是,与其使用nn.linear
,不如从我们的 ResNet 卷积主干中取出并添加一个带有步幅 2 的nn.Conv2d
?这将给我们一个4x4x[# of filters]
张量 - 这里让我们将其设为4x4x(4+c)
,以便得到一个元素数量与我们想要的元素数量完全相等的张量。现在,如果我们创建一个损失函数,接受一个4x4x(4+c)
张量,并将其映射到图像中的 16 个对象,并检查每个对象是否由这些4+c
激活正确表示,这也可以起作用。事实证明,这两种方法实际上都被使用。从一个完全连接的线性层输出一个很长的向量的方法被一类模型使用,这类模型被称为YOLO(You Only Look Once),而卷积激活的方法被一些从SSD(Single Shot Detector)开始的模型使用。由于这些东西在 2015 年末几乎同时出现,事情在很大程度上朝着 SSD 发展。所以今天早上,YOLO 版本 3发布了,现在正在使用 SSD,这就是我们要做的。我们还将了解为什么这样做更有意义。
锚框
假设我们有另一个Conv2d(stride=2)
,那么我们将有一个2x2x(4+c)
张量。基本上,它创建了一个看起来像这样的网格:
这是第二个额外的卷积步幅 2 层激活的几何形状。请记住,步幅 2 卷积对激活的几何形状做的事情与步幅 1 卷积后跟着最大池化假设填充正常的激活几何形状是一样的。
让我们谈谈我们可能在这里做什么。我们希望每个网格单元负责查找图像该部分中最大的对象。
感受野
为什么我们关心每个卷积网格单元负责找到图像相应部分中的事物的想法?原因是因为有一个叫做卷积网格单元的感受野。基本思想是,在您的卷积层中,这些张量的每一部分都有一个感受野,这意味着负责计算该单元的输入图像的哪个部分。就像生活中的所有事物一样,最容易通过 Excel 来看到这一点[38:01]。
取一个激活(在这种情况下是在最大池层)并看看它来自哪里[38:45]。在 Excel 中,您可以执行公式 → 跟踪前导。一直追溯到输入层,您可以看到它来自图像的这个 6 x 6 部分(以及滤波器)。更重要的是,中间部分有很多权重从外部的细胞中出来,而外部的细胞只有一个权重出来。所以我们称这 6 x 6 个单元格为我们选择的一个激活的感受野。
3x3 卷积,不透明度为 15% —— 明显地,盒子的中心有更多的依赖关系
请注意,感受野不仅仅是说这是一个盒子,而且盒子的中心有更多的依赖关系[40:27],当涉及到理解架构以及理解为什么卷积网络工作方式时,这是一个至关重要的概念。
架构 [41:18]
架构是,我们将有一个 ResNet 主干,后面跟着一个或多个 2D 卷积(现在只有一个),这将给我们一个4x4
的网格。
class StdConv(nn.Module): def __init__(self, nin, nout, stride=2, drop=0.1): super().__init__() self.conv = nn.Conv2d( nin, nout, 3, stride=stride, padding=1 ) self.bn = nn.BatchNorm2d(nout) self.drop = nn.Dropout(drop) def forward(self, x): return self.drop(self.bn(F.relu(self.conv(x)))) def flatten_conv(x,k): bs,nf,gx,gy = x.size() x = x.permute(0,2,3,1).contiguous() return x.view(bs,-1,nf//k) class OutConv(nn.Module): def __init__(self, k, nin, bias): super().__init__() self.k = k self.oconv1 = nn.Conv2d( nin, (len(id2cat)+1)*k, 3, padding=1 ) self.oconv2 = nn.Conv2d(nin, 4*k, 3, padding=1) self.oconv1.bias.data.zero_().add_(bias) def forward(self, x): return [ flatten_conv(self.oconv1(x), self.k), flatten_conv(self.oconv2(x), self.k) ] class SSD_Head(nn.Module): def __init__(self, k, bias): super().__init__() self.drop = nn.Dropout(0.25) self.sconv0 = StdConv(512,256, stride=1) self.sconv2 = StdConv(256,256) self.out = OutConv(k, 256, bias) def forward(self, x): x = self.drop(F.relu(x)) x = self.sconv0(x) x = self.sconv2(x) return self.out(x) head_reg4 = SSD_Head(k, -3.) models = ConvnetBuilder(f_model, 0, 0, 0, custom_head=head_reg4) learn = ConvLearner(md, models) learn.opt_fn = optim.Adam • 52
SSD_Head
- 我们从 ReLU 和 dropout 开始
- 然后是步幅为 1 的卷积。我们从步幅为 1 的卷积开始的原因是因为这不会改变几何形状 —— 它只让我们增加一层额外的计算。它让我们不仅可以创建一个线性层,而且现在我们的自定义头部中有一个小型神经网络。
StdConv
在上面定义了 —— 它执行卷积、ReLU、BatchNorm 和 dropout。您看到的大多数研究代码不会像这样定义一个类,而是一遍又一遍地写整个代码。不要这样做。重复的代码会导致错误和理解不足。 - 步幅为 2 的卷积 [44:56]
- 最后,步骤 3 的输出是
4x4
,传递给OutConv
。OutConv
有两个单独的卷积层,每个都是步幅为 1,因此不会改变输入的几何形状。其中一个的长度是类别数(现在忽略k
和+1
是为了“背景” —— 即没有检测到对象),另一个的长度是 4。与其有一个输出4+c
的单个卷积层,不如有两个卷积层并将它们的输出返回到列表中。这使得这些层可以稍微专门化。我们谈到了这样一个想法,当您有多个任务时,它们可以共享层,但它们不必共享所有层。在这种情况下,我们的两个任务是创建一个分类器和创建和创建边界框回归,除了最后一个层外,它们共享每一个层。 - 最后,我们展平卷积,因为 Jeremy 编写的损失函数期望展平的张量,但我们完全可以重写它以不这样做。
Fastai 编码风格 [42:58]
第一版本本周发布。它非常重视阐述性编程的概念,即编程代码应该是您可以用来解释一个想法的东西,理想情况下,可以像数学符号一样容易地向理解您编码方法的人解释。这个想法已经存在很长时间了,但最好的描述是杰里米最崇拜的计算机科学英雄肯·艾弗森在 1979 年的图灵奖演讲中描述的。他在 1964 年之前就一直在研究这个问题,但 1964 年是他发布这种编程方法的第一个例子,称为 APL,25 年后,他获得了图灵奖。然后他把接力棒传给了他的儿子埃里克·艾弗森。Fastai 风格指南是对这些想法的一种尝试。
损失函数[47:44]
损失函数需要查看这 16 组激活中的每一组,每组都有四个边界框坐标和c+1
类概率,并决定这些激活是否接近或远离图像中与该网格单元最接近的对象。如果没有任何东西,那么它是否正确地预测了背景。这是非常难做到的。
匹配问题[48:43]
损失函数需要将图像中的每个对象与这些卷积网格单元中的一个进行匹配,以便说“这个网格单元负责这个特定对象”,然后它可以继续说“好的,这 4 个坐标有多接近,类概率有多接近”。
这是我们的目标[49:56]:
我们的因变量看起来像左边的那个,我们最终的卷积层将是4x4x(c+1)
,在这种情况下c=20
。然后我们将其展平为一个向量。我们的目标是设计一个函数,该函数接受一个因变量和模型输出的一些特定激活,并在这些激活不是地面真实边界框的良好反映时返回更高的数字;或者如果是一个好的反映,则返回更低的数字。
fast.ai 深度学习笔记(四)(3)https://developer.aliyun.com/article/1482738