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.pth
至model_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、总结
最后效果不是很好,好像每次关于图像的处理效果我都不是很理想…