【PyTorch】13 Image Caption:让神经网络看图讲故事

1、数据集获取

数据来自:AI challenger 2017 图像描述数据集
百度网盘: https://pan.baidu.com/s/1g1XaPKzNvOurH9M44p1qrw 提取码: bag3

这里由于原训练集太大,这里仅使用验证集ai_challenger_caption_validation_20170910.zip,解压一下

2、文本数据处理

图像中文描述比赛的数据分为两部分,一是30000张图片,二是对应的描述caption_validation_annotations_20170910.json,每个样本格式如下:

[{"url": "http://img5.cache.netease.com/photo/0005/2013-09-25/99LA1FC60B6P0005.jpg", "image_id": "3cd32bef87ed98572bac868418521852ac3f6a70.jpg", "caption": ["\u4e00\u4e2a\u53cc\u81c2\u62ac\u8d77\u7684\u8fd0\u52a8\u5458\u8dea\u5728\u7eff\u8335\u8335\u7684\u7403\u573a\u4e0a", "\u4e00\u4e2a\u62ac\u7740\u53cc\u81c2\u7684\u8fd0\u52a8\u5458\u8dea\u5728\u8db3\u7403\u573a\u4e0a", "\u4e00\u4e2a\u53cc\u624b\u63e1\u62f3\u7684\u7537\u4eba\u8dea\u5728\u7eff\u8335\u8335\u7684\u8db3\u7403\u573a\u4e0a", "\u4e00\u4e2a\u62ac\u8d77\u53cc\u624b\u7684\u7537\u4eba\u8dea\u5728\u78a7\u7eff\u7684\u7403\u573a\u4e0a", "\u4e00\u4e2a\u53cc\u624b\u63e1\u62f3\u7684\u8fd0\u52a8\u5458\u8dea\u5728\u5e73\u5766\u7684\u8fd0\u52a8\u573a\u4e0a"]}, ...

在这里插入图片描述

"一个双臂抬起的运动员跪在绿茵茵的球场上", "一个抬着双臂的运动员跪在足球场上", "一个双手握拳的男人跪在绿茵茵的足球场上", "一个抬起双手的男人跪在碧绿的球场上", "一个双手握拳的运动员跪在平坦的运动场上"

以上描述的特点:

  • 每一句话长短不一
  • 描述不涉及太多额外知识,尽量客观
  • 尽可能点明图片中的人物关系

这里直接下载人工描述的预处理,包括:

  • 中文jieba分词
  • word2ix,过滤低频词
  • 所有描述补齐到等长(pad_sequence)
  • 利用pack_padded_sequence进行计算加速

但是不能使用caption.pth,因为这里仅使用原来的验证集,所以用书里提供的代码自行处理一下:

# coding:utf8
import torch as t
import numpy as np
import json
import jieba
import tqdm


class Config:
    annotation_file = r'... your path\ai_challenger_caption_validation_20170910\caption_validation_annotations_20170910.json'
    unknown = '</UNKNOWN>'
    end = '</EOS>'
    padding = '</PAD>'
    max_words = 5000
    min_appear = 2
    save_path = r'... your path\ai_challenger_caption_validation_20170910\caption_2.pth'


# START='</START>'
# MAX_LENS = 25,

def process(**kwargs):
    opt = Config()
    for k, v in kwargs.items():
        setattr(opt, k, v)

    with open(opt.annotation_file) as f:
        data = json.load(f)

    # 8f00f3d0f1008e085ab660e70dffced16a8259f6.jpg -> 0
    id2ix = {item['image_id']: ix for ix, item in enumerate(data)}
    # 0-> 8f00f3d0f1008e085ab660e70dffced16a8259f6.jpg
    ix2id = {ix: id for id, ix in (id2ix.items())}
    assert id2ix[ix2id[10]] == 10

    captions = [item['caption'] for item in data]
    # 分词结果
    cut_captions = [[list(jieba.cut(ii, cut_all=False)) for ii in item] for item in tqdm.tqdm(captions)]

    word_nums = {}  # '快乐'-> 10000 (次)

    def update(word_nums):
        def fun(word):
            word_nums[word] = word_nums.get(word, 0) + 1
            return None

        return fun

    lambda_ = update(word_nums)
    _ = {lambda_(word) for sentences in cut_captions for sentence in sentences for word in sentence}

    # [ (10000,u'快乐'),(9999,u'开心') ...]
    word_nums_list = sorted([(num, word) for word, num in word_nums.items()], reverse=True)

    #### 以上的操作是无损,可逆的操作###############################
    # **********以下会删除一些信息******************

    # 1. 丢弃词频不够的词
    # 2. ~~丢弃长度过长的词~~

    words = [word[1] for word in word_nums_list[:opt.max_words] if word[0] >= opt.min_appear]
    words = [opt.unknown, opt.padding, opt.end] + words
    word2ix = {word: ix for ix, word in enumerate(words)}
    ix2word = {ix: word for word, ix in word2ix.items()}
    assert word2ix[ix2word[123]] == 123

    ix_captions = [[[word2ix.get(word, word2ix.get(opt.unknown)) for word in sentence]
                    for sentence in item]
                   for item in cut_captions]
    readme = u"""
    word:词
    ix:index
    id:图片名
    caption: 分词之后的描述,通过ix2word可以获得原始中文词
    """
    results = {
        'caption': ix_captions,
        'word2ix': word2ix,
        'ix2word': ix2word,
        'ix2id': ix2id,
        'id2ix': id2ix,
        'padding': '</PAD>',
        'end': '</EOS>',
        'readme': readme
    }
    t.save(results, opt.save_path)
    print('save file in %s' % opt.save_path)

    def test(ix, ix2=4):
        results = t.load(opt.save_path)
        ix2word = results['ix2word']
        examples = results['caption'][ix][4]
        sentences_p = (''.join([ix2word[ii] for ii in examples]))
        sentences_r = data[ix]['caption'][ix2]
        assert sentences_p == sentences_r, 'test failed'

    test(1000)
    print('test success')


if __name__ == '__main__':
    process()

得到了一个caption_2.pth文件,一个使用的例子:

import torch

data = torch.load(r'... your path\ai_challenger_caption_validation_20170910\caption_2.pth')
ix2word = data['ix2word']
ix2id = data['ix2id']
caption = data['caption']

img_ix = 0
img_caption = caption[img_ix]

print(ix2id[img_ix])
print(img_caption)

sen = img_caption[0]
sen = [ix2word[_] for _ in sen]
str = ''.join(sen)
print(str)
3cd32bef87ed98572bac868418521852ac3f6a70.jpg
[[4, 178, 79, 3, 47, 159, 5, 112, 3, 20], [4, 176, 178, 3, 47, 159, 5, 64, 6], [4, 19, 361, 3, 7, 159, 5, 112, 3, 64, 6], [4, 79, 19, 3, 7, 159, 5, 124, 3, 20], [4, 19, 361, 3, 47, 159, 5, 65, 3, 26, 6]]
一个双臂抬起的运动员跪在绿茵茵的球场上

3、图像数据处理

利用ResNet在池化层的输出、全连接层的输入,这里复制并修改ResNet的源码,在倒数第二层就输出返回,修改完ResNet后,提取3万张图片的特征,代码如下:

from torchvision.models import resnet50

def new_forward(self, x):
    x = self.conv1(x)
    x = self.bn1(x)
    x = self.relu(x)
    x = self.maxpool(x)

    x = self.layer1(x)
    x = self.layer2(x)
    x = self.layer3(x)
    x = self.layer4(x)

    x = self.avgpool(x)
    x = x.view(x.size(0), -1)
    # x = self.fc(x)
    return x

model = resnet50(pretrained=True)
model.forward = lambda x:new_forward(model, x)
model = model.cuda()

import torchvision as tv  # 一般的图像转换操作类
from PIL import Image  # pillow库,PIL读取图片
import numpy as np
import torch
from torch.utils import data
import os

IMAGENET_MEAN = [0.485, 0.456, 0.406]
IMAGENET_STD = [0.229, 0.224, 0.225]
normalize = tv.transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD)
transforms = tv.transforms.Compose([
    tv.transforms.Resize(256),
    tv.transforms.CenterCrop(256),
    tv.transforms.ToTensor(),
    normalize
])


class Dataset(data.Dataset):
    def __init__(self, caption_data_path):
        data = torch.load(
            '/mnt/Data1/ysc/ai_challenger_caption_validation_20170910/caption_2.pth')
        self.ix2id = data['ix2id']
        self.imgs = [os.path.join(caption_data_path, self.ix2id[_]) for _ in range(len(self.ix2id))]

    def __getitem__(self, item):
        x = Image.open(self.imgs[item]).convert('RGB')
        x = transforms(x)  # ([3, 256, 256])
        return x, item

    def __len__(self):
        return len(self.imgs)


batch_size = 32
dataset = Dataset(
    '/mnt/Data1/ysc/ai_challenger_caption_validation_20170910/caption_validation_images_20170910')
dataloader = data.DataLoader(dataset, batch_size=batch_size, shuffle=False)

results = torch.Tensor(len(dataloader.dataset), 2048).fill_(0)

for ii, (imgs, indexs) in enumerate(dataloader):
    assert indexs[0] == batch_size * ii
    imgs = imgs.cuda()
    features = model(imgs)
    results[ii * batch_size:(ii + 1) * batch_size] = features.data.cpu()
    print(ii * batch_size)

torch.save(results, '/mnt/Data1/ysc/ai_challenger_caption_validation_20170910/results_2048.pth')

这里得到了原本ResNet的1000维特征向量results.pth和修改最后线性层的2048维向量results_2048.pth(torch.Size([30000, 2048]))

4、训练

关于np.random.choice函数:

np.random.choice(5, 3)
>> array([0, 3, 4]) # random

关于pack_padded_sequence可见

关于beam search的代码可见书的官网

最大迭代次数为5,每100代查看一次结果:

'一个 一个 的 男人 </EOS>'
'一个 穿着 的 男人 </EOS>'
'一个 穿着 球衣 的 男人 在 运动场 上 </EOS>'
'一个 穿着 球衣 的 男人 在 运动场 上 </EOS>'
'一个 穿着 球衣 的 男人 在 运动场 上 踢足球 </EOS>'
'两个 穿着 球衣 的 男人 在 球场上 踢足球 </EOS>'
'两个 穿着 运动服 的 男人 在 球场上 踢足球 </EOS>'
'一个 右手 拿 着 话筒 的 男人 在 舞台 上 表演 </EOS>'
'一个 戴着 帽子 的 男人 在 舞台 上 表演 </EOS>'
'一个 戴着 帽子 的 男人 在 舞台 上 唱歌 </EOS>'
'一个 戴着 帽子 的 男人 和 一个 戴着 帽子 的 男人 站 在 舞台 上 </EOS>'
'一个 戴着 帽子 的 男人 在 舞台 上 表演 节目 </EOS>'
'一个 戴着 帽子 的 女人 在 舞台 上 表演 节目 </EOS>'
'一个 戴着 帽子 的 女人 在 舞台 上 表演 节目 </EOS>'
'一个 戴着 帽子 的 男人 在 舞台 上 表演 </EOS>'
'一个 戴着 帽子 的 男人 在 舞台 上 唱歌 </EOS>'
'一个 戴着 帽子 的 男人 和 一个 戴着 帽子 的 男人 站 在 草地 上 </EOS>'
'一个 戴着 帽子 的 男人 和 一个 戴着 帽子 的 男人 在 舞台 上 表演 </EOS>'
'一个 戴着 帽子 的 男人 和 一个 戴着 帽子 的 男人 站 在 草地 上 </EOS>'
'一个 戴着 帽子 的 男人 和 一个 戴着 帽子 的 男人 站 在 道路 上 </EOS>'
...

在这里插入图片描述
这里每次迭代都保存了模型,名字分别为model_0.pthmodel_4.pth

这里自己找一下照片测试一下:

在这里插入图片描述

'一个 穿着 球衣 的 男人 在 球场上 踢足球 </EOS>'
'一个 穿着 黑色 上衣 的 男人 和 一个 戴着 帽子 的 男人 站 在 道路 上 </EOS>'
'一个 戴着 帽子 的 男人 在 舞台 上 表演 </EOS>'
'一个 戴着 帽子 的 男人 和 一个 戴着 帽子 的 男人 走 在 道路 上 </EOS>'
'一个 右手 拿 着 话筒 的 男人 在 舞台 上 唱歌 </EOS>'

结果有点bug,修改一下最大迭代次数为100:

在这里插入图片描述
在这里插入图片描述
以及再怎么修改隐层层数、个数,结果都没有太好,猜测可能的原因在于Beam Search搜索、本身数据集很脏…

最大迭代次数为50:

在这里插入图片描述
在这里插入图片描述

5、全部代码

import torch
from torch.utils import data
import numpy as np
import tqdm
from torch.nn.utils.rnn import pack_padded_sequence
from beam_search import CaptionGenerator
from PIL import Image
import torchvision as tv
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
from torchvision.models import resnet50
from torch.utils.data.dataset import random_split


def new_forward(self, x):
    x = self.conv1(x)
    x = self.bn1(x)
    x = self.relu(x)
    x = self.maxpool(x)

    x = self.layer1(x)
    x = self.layer2(x)
    x = self.layer3(x)
    x = self.layer4(x)

    x = self.avgpool(x)
    x = x.view(x.size(0), -1)
    # x = self.fc(x)
    return x

model_feature = resnet50(pretrained=True)
model_feature.forward = lambda x:new_forward(model_feature, x)
model_feature = model_feature.cuda()

IMAGENET_MEAN = [0.485, 0.456, 0.406]
IMAGENET_STD = [0.229, 0.224, 0.225]
normalize = tv.transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD)
transforms = tv.transforms.Compose([
    tv.transforms.Resize(256),
    tv.transforms.CenterCrop(256),
    tv.transforms.ToTensor(),
    normalize
])

class CaptionDataset(data.Dataset):
    def __init__(self):
        data = torch.load('/mnt/Data1/ysc/ai_challenger_caption_validation_20170910/caption_2.pth')
        # ix2word = data['ix2word']
        self.ix2id = data['ix2id']
        self.caption = data['caption']
        word2ix = data['word2ix']
        self.padding = word2ix.get(data.get('padding'))
        self.end = word2ix.get(data.get('end'))

        self.feature = torch.load('/mnt/Data1/ysc/ai_challenger_caption_validation_20170910/results_2048.pth')

    def __getitem__(self, item):
        img = self.feature[item]
        caption = self.caption[item]
        rdn_index = np.random.choice(len(caption), 1)[0]        # 5句描述随机选一句
        caption = caption[rdn_index]
        return img, torch.LongTensor(caption), item

    def __len__(self):
        return len(self.ix2id)

def create_collate_fn(padding, eos, max_length=50):
    def collate_fn(img_cap):
        """
        将多个样本拼接在一起成一个batch
        输入: list of data,形如
        [(img1, cap1, index1), (img2, cap2, index2) ....]

        拼接策略如下:
        - batch中每个样本的描述长度都是在变化的,不丢弃任何一个词\
          选取长度最长的句子,将所有句子pad成一样长
        - 长度不够的用</PAD>在结尾PAD
        - 没有START标识符
        - 如果长度刚好和词一样,那么就没有</EOS>

        返回:
        - imgs(Tensor): batch_sie*2048
        - cap_tensor(Tensor): batch_size*max_length (I think it is wrong!)
        - lengths(list of int): 长度为batch_size
        - index(list of int): 长度为batch_size
        """
        img_cap.sort(key=lambda p: len(p[1]), reverse=True)
        imgs, caps, indexs = zip(*img_cap)
        imgs = torch.cat([img.unsqueeze(0) for img in imgs], 0)     # batch * 2048
        lengths = [min(len(c) + 1, max_length) for c in caps]
        batch_length = max(lengths)
        cap_tensor = torch.LongTensor(batch_length, len(caps)).fill_(padding)
        for i, c in enumerate(caps):
            end_cap = lengths[i] - 1
            if end_cap < batch_length:
                cap_tensor[end_cap, i] = eos
            cap_tensor[:end_cap, i].copy_(c[:end_cap])
        return (imgs, (cap_tensor, lengths), indexs)        # batch * 2048, (max_len * batch, ...), ...

    return collate_fn


batch_size = 32
max_epoch = 50
embedding_dim = 64
hidden_size = 64
lr = 1e-4
num_layers = 2

def get_dataloader():
    dataset = CaptionDataset()
    n_train = int(len(dataset) * 0.9)
    split_train, split_valid = random_split(dataset=dataset, lengths=[n_train, len(dataset) - n_train])
    train_dataloader = data.DataLoader(split_train, batch_size=batch_size, shuffle=True, num_workers=4,
                                 collate_fn=create_collate_fn(dataset.padding, dataset.end))
    valid_dataloader = data.DataLoader(split_valid, batch_size=batch_size, shuffle=True, num_workers=4,
                                       collate_fn=create_collate_fn(dataset.padding, dataset.end))
    return train_dataloader, valid_dataloader


class Net(torch.nn.Module):
    def __init__(self, word2ix, ix2word):
        super(Net,self).__init__()
        self.ix2word = ix2word
        self.word2ix = word2ix
        self.embedding = torch.nn.Embedding(len(word2ix), embedding_dim)
        self.fc = torch.nn.Linear(2048, hidden_size)
        self.rnn = torch.nn.LSTM(embedding_dim, hidden_size, num_layers=num_layers)
        self.classifier = torch.nn.Linear(hidden_size, len(word2ix))

    def forward(self, img_feats, captions, lengths):
        embeddings = self.embedding(captions)       # seq_len * batch * embedding
        img_feats = self.fc(img_feats).unsqueeze(0)     # img_feats是2048维的向量,通过全连接层转为256维的向量,和词向量一样, 1 * batch * hidden_size
        embeddings = torch.cat([img_feats, embeddings], 0)      # 将img_feats看成第一个词的词向量, (1+seq_len) * batch * hidden_size
        packed_embeddings = pack_padded_sequence(embeddings, lengths)       # PackedSequence, lengths - batch中每个seq的有效长度
        outputs, state = self.rnn(packed_embeddings)    # seq_len * batch * (1*256), (1*2) * batch * hidden_size, lstm的输出作为特征用来分类预测下一个词的序号, 因为输入是PackedSequence,所以输出的output也是PackedSequence, PackedSequence第一个元素是Variable,第二个元素是batch_sizes,即batch中每个样本的长度*
        pred = self.classifier(outputs[0])
        return pred, state

    def generate(self, img, eos_token='</EOS>', beam_size=3, max_caption_length=30, length_normalization_factor=0.0):   # 根据图片生成描述,主要是使用beam search算法以得到更好的描述
        cap_gen = CaptionGenerator(embedder=self.embedding,
                                   rnn=self.rnn,
                                   classifier=self.classifier,
                                   eos_id=self.word2ix[eos_token],
                                   beam_size=beam_size,
                                   max_caption_length=max_caption_length,
                                   length_normalization_factor=length_normalization_factor)
        if next(self.parameters()).is_cuda:
            img = img.cuda()
        img = img.unsqueeze(0)
        img = self.fc(img).unsqueeze(0)
        sentences, score = cap_gen.beam_search(img)
        sentences = [' '.join([self.ix2word[idx.item()] for idx in sent])
                     for sent in sentences]
        return sentences



def evaluate(dataloader):
    model.eval()
    total_loss = 0
    with torch.no_grad():
        for ii, (imgs, (captions, lengths), indexes) in enumerate(dataloader):
            imgs = imgs.to(device)
            captions = captions.to(device)
            input_captions = captions[:-1]
            target_captions = pack_padded_sequence(captions, lengths)[0]
            score, _ = model(imgs, input_captions, lengths)
            loss = criterion(score, target_captions)
            total_loss += loss.item()
    model.train()
    return total_loss

if __name__ == '__main__':
    train_dataloader, valid_dataloader = get_dataloader()
    _data = torch.load('/mnt/Data1/ysc/ai_challenger_caption_validation_20170910/caption_2.pth')
    word2ix, ix2word = _data['word2ix'], _data['ix2word']

    # max_loss = float('inf')     # 221
    max_loss = 263

    device = torch.device('cuda')


    model = Net(word2ix, ix2word)
    optimizer = torch.optim.Adam(model.parameters(), lr=lr)
    criterion = torch.nn.CrossEntropyLoss()

    model.to(device)

    losses = []
    valid_losses = []


    img_path = '/mnt/Data1/ysc/ai_challenger_caption_validation_20170910/123.jpg'
    raw_img = Image.open(img_path).convert('RGB')
    raw_img = transforms(raw_img)  # 3*256*256
    img_feature = model_feature(raw_img.cuda().unsqueeze(0))
    print(img_feature)

    for epoch in range(max_epoch):
        for ii, (imgs, (captions, lengths), indexes) in tqdm.tqdm(enumerate(train_dataloader)):
            optimizer.zero_grad()
            imgs = imgs.to(device)
            captions = captions.to(device)
            input_captions = captions[:-1]
            target_captions = pack_padded_sequence(captions, lengths)[0]
            score, _ = model(imgs, input_captions, lengths)
            loss = criterion(score, target_captions)
            loss.backward()
            optimizer.step()
            losses.append(loss.item())

            if (ii + 1) % 20 == 0:  # 可视化
                # 可视化原始图片 + 可视化人工的描述语句
                # raw_img = _data['ix2id'][indexes[0]]
                # img_path = '/mnt/Data1/ysc/ai_challenger_caption_validation_20170910/caption_validation_images_20170910/' + raw_img
                # raw_img = Image.open(img_path).convert('RGB')
                # raw_img = tv.transforms.ToTensor()(raw_img)
                #
                # raw_caption = captions.data[:, 0]
                # raw_caption = ''.join([_data['ix2word'][ii.item()] for ii in raw_caption])
                #
                # results = model.generate(imgs.data[0])
                #
                # print(img_path, raw_caption, results)
                #
                #
                # print(model.generate(img_feature.squeeze(0)))
                tmp = evaluate(valid_dataloader)
                valid_losses.append(tmp)
                if tmp < max_loss:
                    max_loss = tmp
                    torch.save(model.state_dict(),
                               '/mnt/Data1/ysc/ai_challenger_caption_validation_20170910/model_best.pth')
                    print(max_loss)     # 190 111
    plt.figure(1)
    plt.plot(losses)
    plt.figure(2)
    plt.plot(valid_losses)
    plt.show()


    # model.load_state_dict(torch.load('/mnt/Data1/ysc/ai_challenger_caption_validation_20170910/model_best.pth'))
    # print(model.generate(img_feature.squeeze(0)))

6、总结

最后效果不是很好,好像每次关于图像的处理效果我都不是很理想…

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值