Skip to content

필사 모드: 人人可懂的 AI 第4篇 — 用137万参数给图像配句子,以及第3篇的 bug 为何没出现在这里

中文
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

引言 — 图像描述与 VQA 有何不同

第3篇的 VQA 接收图像和问题,给出简短答案。本篇没有问题, 只看图像就写出一句话

[运动鞋图像]  ->  "a photo of a sneaker"
[裤子图像]    ->  "trousers on white background"

差别在输出长度。VQA 的答案是六个字符,字幕是四十个。变长了,结构就得变。第3篇把图像、问题、答案连成一条序列;这里我们 把编码器和解码器分开

数据 — 怎样给图像配上句子

Fashion-MNIST 没有字幕,只有 0 到 9 的标签。于是我们做了把标签变成句子的模板。

ARTICLES = ["t-shirt", "trousers", "a pullover", "a dress", "a coat",
            "a sandal", "a shirt", "a sneaker", "a bag", "an ankle boot"]
TEMPLATES = ["a photo of {}", "this looks like {}", "{} on white background"]

每张图随机配三种模板之一。这样模型就必须 学结构,而不是把整句背下来 :以 "a photo of" 开头后面跟品名,以品名开头后面跟 "on white background"。

冠词也故意做得不规则。t-shirttrousers 不带冠词,只有 an ankle bootan。我们借此还能检查模型是否答对。

结构 — 拆分编码器与解码器

class Captioner(nn.Module):
    def __init__(self):
        super().__init__()
        self.cnn = nn.Sequential(
            nn.Conv2d(1, 32, 3, 2, 1), nn.GELU(),
            nn.Conv2d(32, 64, 3, 2, 1), nn.GELU(),
            nn.Conv2d(64, 128, 3, 2, 1), nn.GELU(),
        )
        self.proj = nn.Linear(128, 160)
        self.emb = nn.Embedding(V, 160)
        self.pos = nn.Embedding(CAP, 160)
        layer = nn.TransformerDecoderLayer(160, 5, 640, batch_first=True, norm_first=True)
        self.dec = nn.TransformerDecoder(layer, 3)
        self.head = nn.Linear(160, V)
        self.register_buffer("m", torch.triu(torch.full((CAP, CAP), float("-inf")), 1))

    def forward(self, img, cap_in):
        mem = self.proj(self.cnn(img).flatten(2).transpose(1, 2))
        T = cap_in.shape[1]
        h = self.emb(cap_in) + self.pos(torch.arange(T, device=cap_in.device))
        return self.head(self.dec(h, mem, tgt_mask=self.m[:T, :T]))

核心就是 self.dec(h, mem, ...) 这一行。nn.TransformerDecoder 接收两个输入:

  • h —— 目前已生成的字幕 token
  • mem —— CNN 产出的 16 个图像向量 (memory)

在每个解码层内部,两种注意力依次发生。先是 self-attention ,字幕查看自己的过去 —— 这正是需要 tgt_mask 的原因。然后是 cross-attention ,字幕的每个位置查看那 16 个图像向量。

第二种很关键。生成 "sneaker" 这个词时,模型自己决定该看图像的哪个区域。像第3篇那样把一切塞进同一条序列,图像和文本就共用同一份注意力;而这里 职责被分开,各自独立优化 。字幕越长,这种分离越划算。

第3篇的 bug 为何没出现在这里

让第3篇跌到 7.5% 的元凶,是构造目标序列的函数。把两段并排看。

第3篇 (有 bug)

def tok(s, L):
    ids = [enc[c] for c in s][:L]
    return ids + [0] * (L - len(ids))

a_full = torch.tensor([tok(s, AL - 1) + [1] for s in ans])   # 先填充后加 EOS

第4篇 (正确)

def tok(s):
    ids = [enc[c] for c in s][:CAP - 1] + [1]   # 先加 EOS
    return ids + [0] * (CAP - len(ids))          # 再填充

把 "even" 分别喂进去:

结果"n" 之后那格的目标
第3篇[e, v, e, n, PAD, EOS]PAD → 被排除在损失外
第4篇[e, v, e, n, EOS, PAD]EOS → 计入损失

只差一个顺序。第4篇先加 EOS 再填充,所以 EOS 总是落在答案结束的位置。模型能学会停下来,也就没碰上那个 bug。

同一个人同一天写的两段代码,只有一段错了。 这类错误在评审中很难抓到,指标也看不见 —— 第3篇里损失反而更好看。除了直接看输出,没有别的办法。

训练日志

[   12.6s] params {"params": 1367262, "cap_len": 40}
[   13.3s] step      0 loss=3.7440
[   14.2s] step    100 loss=0.1660
[  608.4s] SUMMARY {"steps": 79504, "final_loss": 0.0468, "caption_acc": 0.91}

100 步之内 —— 也就一秒多 —— 损失从 3.74 掉到 0.166。字幕只有 30 种组合 (10 品类 × 3 模板),结构很快就被摸清了。

结果

我们按 生成的字幕中是否出现正确品名 对 300 张测试图评分,结果是 91%。

[an ankle boot] -> an ankle boot on white background
[a pullover]    -> a photo of a pullover
[trousers]      -> trousers on white background
[trousers]      -> this looks like trousers
[a shirt]       -> a photo of a shirt
[a coat]        -> a photo of a coat

有几点值得注意。

模板会交替出现。 同样是 trousers,一次给出 "trousers on white background",另一次给出 "this looks like trousers"。说明它学的是三种结构,而不是背下一句。

冠词是对的。 an ankle bootana pullovera,以及 trousers 前不加冠词,全都正确。以字符为单位生成还能做到这一点,说明它学到了品名首字母与冠词之间的关系。

错的那 9% 主要集中在衬衫类。 在 Fashion-MNIST 中,shirtt-shirtcoatpullover 在 28×28 灰度下很难分辨。人也会混淆,所以这类错误更接近数据的极限,而非模型的缺陷。

小结

项目
参数量1,367,262 (1.37M)
训练时间608.4秒
步数79,504
最终损失0.0468
字幕标签命中率0.91

输出越长,编码器-解码器分离越值。cross-attention 让每个正在生成的词只查阅图像中它需要的部分,图像表征也不再与语言表征争抢容量。

与第3篇的对照是本篇的另一个收获。同样的错误近在咫尺,只是碰巧先加了 EOS 才躲过去。 要摆脱运气,就把构造目标的函数输出一次看看。 取一个批次调用 print,十秒钟的事。

下一篇转向输出是 图像本身 的情形 —— 给黑白照片上色。在那里我们会看到损失函数的选择如何改变结果的性格。

🧠 理解度自测

1. nn.TransformerDecoder 内部的两种注意力各自在做什么?

第一种是 self-attention,正在生成的字幕参照自己此前的 token —— 因果掩码正是用在这里。第二种是 cross-attention,字幕的每个位置参照 CNN 产出的 16 个图像向量,让模型自行决定写某个词时该看图像的哪个区域。

2. 为什么要混用三种字幕模板?

只用一种的话,模型可以把整句背下来。混用模板迫使它学结构:"a photo of" 之后是品名,品名之后是 "on white background"。同一张 trousers 图像生成了不同模板,证实了这一点。

3. 第3篇的 EOS bug 为什么没出现在第4篇的代码里?

第3篇用 tok(s, AL-1) + [1],先填充再追加 EOS,于是填充挤在了答案和 EOS 之间。第4篇用 [enc[c] for c in s][:CAP-1] + [1],先加 EOS 再填充,EOS 总落在答案结束处。只差一个顺序。

4. 错误集中在衬衫类的那 9%,该算作模型缺陷吗?

不能全算。Fashion-MNIST 中 shirt、t-shirt、coat、pullover 在 28×28 灰度下连人都难以分辨。这更接近数据所携带信息量的上限,也不是把模型做大就能解决的那类错误。

参考资料

현재 단락 (1/86)

第3篇的 VQA 接收图像和问题,给出简短答案。本篇没有问题, **只看图像就写出一句话** 。

작성 글자: 0원문 글자: 4,060작성 단락: 0/86