前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Pytorch神器(9)

Pytorch神器(9)

作者头像
刀刀老高
发布2018-07-24 14:20:21
7830
发布2018-07-24 14:20:21
举报
文章被收录于专栏:奇点大数据奇点大数据

大家好,今天我们来看Pytorch神器第九次分享的内容——Image Caption。

老实说,这一讲的内容比起前面入门的基础来说会复杂上很多,因为从模型的角度来说基本是我们学过的各种深度学习组件的堆叠,所以主要的难度我认为集中在工程层面。为了让整个的讲解显得流畅且不那么突兀,还是先让大家又感性认识开始比较好。

1、 问题题设

我们先来看看一个叫做captionbot的AI应用吧。

http://www.captionbot.ai

这是微软的杰作,一个可以“读懂”图片内容的应用。

我们先来看看它的表现:

看上去还不错是吧?嗯,不过也有它糊里糊涂的时候:

如果你自己玩一下你也会发现,这种应用很有趣,不论它在真实的应用场景中究竟能有什么作为,但是无可争议的是,它在一定程度确实能够识别出来图片中的物体及其他们的关系。

2、 基础与原理

要想实现这样一个应用,方法不是一种。关于图片标注的论文也有很多了,而且每年都有,不过其中一个比较著名的项目叫做Nerualtalk2还是非常值得我们关注的。

官网位置:

http://cs.stanford.edu/people/karpathy/deepimagesent/

论文位置:

http://arxiv.org/abs/1412.2306

论文的作者之一就是大名鼎鼎的李飞飞老师。

由其官方(也就是Karpathy本人——论文的另一作者)发布的有关此论文实现的代码在:

https://github.com/karpathy/neuraltalk2

效果是真的不错,不过比较遗憾的是它是用Python+Torch实现的,可不是我们现在说的Pytorch。

原理示意图:

在论文中,作者画了这样一张图。

简单来说,一张图片进入模型后,要通过两种重要的组件来做提取和拟合。

一种是卷积层或者卷积网络,用来提取图片上的特征点,形成FeatureMap;

一种是循环神经层(或者双向循环神经层),用来把输入的FeatureMap的Code——也就是那些代表了输入内容的编码值进行带有前后之间关系的拟合;

最终输出一个通顺的句子,并且把这个句子和标准的标签句子进行对比评分,和标签最为接近的句子就是最好的句子。

以上信息基本提供了最为基本的构思模型,也就是我们一直说的四部曲的要求。

输入:一张图片

输出:一个句子

网络结构:CNN+RNN

损失函数:输出的句子与标准句子的差距

在上次的分享中,我们着重提了Encoding和Decoding的思维方式。在这个项目中同样是这样一种思想来起作用。Encoding的部分就是一张图片输入后变成一个FeaureMap的过程;Decoding的部分就是这个FeatureMap通过RNN变成一个人能读懂的句子的过程。

3、 工程代码

在Github上,用Pytorch实现ImageCaption的工程代码非常少,其中有一个的位置在这里:

https://github.com/ruotianluo/ImageCaptioning.pytorch

我对这段代码进行了一些改造,做了这个实验并给大家做个分享,修改后的代码会在稍后发出来,在这里我就只说这个项目如何使用。

(1) 下载这个项目

git clone https://github.com/ruotianluo/ImageCaptioning.pytorch

(2) 准备训练与测试数据

建立一个专门放训练和测试数据的文件夹

mkdir ~/imageroot

下载COCO2014的数据集,如果你不知道去哪里下请留言,我给你一个网盘地址。不过事先声明一下,如果你没有给某度交过保护费的话,可能下载就像龟爬一样。

解压缩COCO2014的数据集

cd ~/imageroot

unzip train2014.zip

unzip val2014.zip

设置相应的环境变量地址,将IMAGE_ROOT指向刚刚我们建立的文件夹

export IMAGE_ROOT=~/imageroot

(3) 准备标注数据集

下载

wget http://cs.stanford.edu/people/karpathy/deepimagesent/caption_datasets.zip

解压缩

unzip caption_datasets.zip

你会得到三个文件,就像我这样

-rw-rw-r-- 1 gao gao 144186139 11月 26 2014 dataset_coco.json

-rw-rw-r-- 1 gao gao 38318553 11月 25 2014 dataset_flickr30k.json

-rw-rw-r-- 1 gao gao 9035673 11月 24 2014 dataset_flickr8k.json

这个模型是支持训练flickr数据集的,也支持在COCO数据集上训练。我们这次实验是在COCO数据集上做,也就是第一个dataset_coco.json文件所对应的数据集。

这个jsonw文件居然有144MB大,如果你用文本编辑器打开,即便是使用UltraEdit这样的工具也会感觉非常卡。

为了说明这个文件的内容,我把这个文件做了节选:

只把前几“行”拿出来做解析。

其实行这个说法非常不严谨,因为这个json文件其实只有一行而已,中间都是用json的分隔符隔开的key-value结构,那我们就只看看第一个结构里说了什么吧。

看一下内容:

{"filepath": "val2014",

"sentids": [770337, 771687,772707, 776154, 781998],

"filename":"COCO_val2014_000000391895.jpg",

"imgid": 0,

"split": "test",

"sentences": [

{"tokens": ["a","man", "with", "a", "red","helmet", "on", "a", "small","moped", "on", "a", "dirt", "road"],"raw": "A man with a red helmet on a small moped on a dirt road.", "imgid": 0, "sentid": 770337},

{"tokens": ["man","riding", "a", "motor", "bike","on", "a", "dirt", "road","on", "the", "countryside"], "raw":"Man riding a motor bike on a dirt road on the countryside.","imgid": 0, "sentid": 771687},

{"tokens": ["a","man", "riding", "on", "the","back", "of", "a", "motorcycle"],"raw": "A man riding on the back of a motorcycle.", "imgid":0, "sentid": 772707},

{"tokens": ["a","dirt", "path", "with", "a","young", "person", "on", "a","motor", "bike", "rests", "to","the", "foreground", "of", "a","verdant", "area", "with", "a", "bridge","and", "a", "background", "of","cloud", "wreathed", "mountains"],"raw": "A dirt path with a young person on a motor bike rests tothe foreground of a verdant area with a bridge and a background ofcloud-wreathed mountains. ", "imgid": 0, "sentid":776154},

{"tokens": ["a","man", "in", "a", "red","shirt", "and", "a", "red","hat", "is", "on", "a","motorcycle", "on", "a", "hill","side"], "raw": "A man in a red shirt and a red hat ison a motorcycle on a hill side.", "imgid": 0, "sentid":781998}],

"cocoid": 391895}

从结构上来看似乎并不太复杂,

前半截描述了这个json段所描述的对象文件名,路径等信息。

后半截描述的是标注的语句信息,注意一个图片标注的可不是一句,而是好几句。

A man with a red helmet on a small moped ona dirt road.

Man riding a motor bike on a dirt road onthe countryside.

A man riding on the back of a motorcycle.

A dirt path with a young person on a motorbike rests to the foreground of a verdant area with a bridge and a backgroundof cloud-wreathed mountains.

A man in a red shirt and a red hat is on amotorcycle on a hill side.

我们读读看就能知道,这应该是不同人看了同样一张图片之后所做的描述信息。由于每个人的观察和思维方式不同以及语言表达本身的自由性,一张图片所描述出来的信息就是会像这样有所不同。

所有的图片都是按照类似这样的方式进行标注的,这就是我们的训练集了。也就是最后输出的Label已经准备好了。

(4) 准备下载预训练模型

在这个位置:

https://drive.google.com/drive/folders/0B7fNdx_jAqhtbVYzOURMdDNHSGM

有三个可以下载的模型:

resnet50.pth

resnet101.pth

resnet152.pth

它们分别是50层、101层和152层的Residual Network——也叫残差网络。

这里说的残差网络(Residual Network)是跟我们平时说的平网络(Plain Network)是相对的。所谓平网络,就是像前面分享中见到的那种一层一层把数据从前往后传递的那种网络。而残差网络有所不同。

从示意图上可以看出来,残差网络的信息传递多了一个“途径”。和普通的平网络不同,它在每两层之间加入了一个Shortcut连接方式,也就是让这一层的卷积核(第n层)不仅要把n-1层的输出作为输入,还要把n-2层的输出也作为输入。这种方法的好处是会使得误差的传递更为直接,不容易让网络达到饱和,也能使得网络变得格外深,进而使得网络能够有非常好的记忆能力。

需要强调的是,在这个实验中,作为Encoder的部分,使用残差网络也可以,使用传统的VGG网络或者其它网络也可以,只是通常我们认为残差网络的记忆能力更强,Encode的效果更好而已。

在这里我下载的是resnet101.pth这个模型——一个101层的残差网络。

建立相应的文件夹

mkdir~/ImageCaptioning.pytorch/data/imagenet_weights

把这个模型文件拷贝进去

cp ~/Downloads/resnet101.pth~/ImageCaptioning.pytorch/data/imagenet_weights

注意:

我们下载的这个resnet101.pth模型文件是一个在分类任务上做过训练的模型文件,已经具备了相当的特征提取的能力。在上次的分享中我们已经着重提过这个问题了,当一个网络模型在一个图片分类的任务上已经做过训练以后就具备了这样的能力。什么能力呢?那就是导数第二层之前的部分具有对图片的特征提取的能力,它们对图片上的物体的边缘轮廓、颜色、大小等有了区分的能力,可以使得不同的物体在倒数第二层输出的FeatureMap上把不同的物体哈希到不同的code分布上面去。这个提取能力就是在训练迭代中它逐渐得到的。

(5) 安装依赖包

由于工程的需要,我们要安装这样两个包

pip3 install h5py

pip3 install scikit-image

h5py是为了用来让python读写h5文件的。这里说的h5文件指的是一种叫做HDF5(Hierarchical Data Format)的文件格式,是一种针对大量数据进行组织和存储的文件格式。在深度学习应用中会经常使用到——因为在读写大文件的时候,用python直接操作文本文件是会死人的……

scikit-image是一个开源的用来处理图像的包,也是在深度学习中经常使用的一个辅助工具。

(6) 标签文件预处理

设置PYTHONPATH

exportPYTHONPATH=~/ImageCaptioning.pytorch/

调用prepro_labels.py文件

python scripts/prepro_labels.py--input_json data/dataset_coco.json --output_json data/cocotalk.json--output_h5 data/cocotalk

通过调用prepro_lables.py文件,标签句子就被生成到了data/cocotalk_label.h5文件中去。

调用prepro_feats.py文件

python scripts/prepro_feats.py --input_jsondata/dataset_coco.json --output_dir data/cocotalk --images_root $IMAGE_ROOT

通过调用prepro_feats.py文件,每个用于训练的COCO数据集的图片文件都像过筛子一样通过了resnet101.pth模型的正向传播,进行了特征提取,并且把提取完的特征放到了两个不同的文件中去。data/cocotalk_fc里放的是全连接层后产生的分布值,data/cocotalk_att放的是最后一个卷积层的输出值,这两个都是特征提取的内容。

一会儿再在训练的时候实际上我们只训练从code(提取过的图形特征编码)到sentence之间的部分,也就是训练Decoder这个部分就可以了。不过这两个命令所用的时间非常长,尤其是data/cocotalk_att的生成。其产生的文件也非常大,在我的计算机上产生了一个超过300GB的h5文件。这个h5文件中记录就是每个训练样本图片的code——特征表示。

注意:实际上整个项目的构成你可以理解成两个独立的完整的项目题设。当前这个题设合理地利用了在Resnet101上做分类这个项目的结果而已。以后类似的情况在别的项目中也会见到,这是一种非常好的组合方式,非常有借鉴意义。

(7) 准备cocotools

在开始训练之前还要准备cocotools,就是用来读写COCO数据集的python程序接口。

cd ~/

git clone https://github.com/tylin/coco-caption

cd coco-caption

cp -r pycocotools/../ImageCaptioning.pytorch/

这样就把下载的coco-caption项目的pycocotools这个文件夹的内容都拷贝到ImageCaption这个项目中去了。

(8) 开始训练

cd ~/ImageCaptioning.pytorch/

python train.py --id st --caption_modelshow_tell --input_json data/cocotalk.json --input_fc_dir data/cocotalk_fc--input_att_dir data/cocotalk_att --input_label_h5 data/cocotalk_label.h5--batch_size 10 --learning_rate 5e-4 --learning_rate_decay_start 0--scheduled_sampling_start 0 --checkpoint_path log_st --save_checkpoint_every6000 --val_images_use 5000 --max_epochs 25

命令并不难,CTRL+C和CTRL+V就能解决了。具体的参数可以参考opts.py里的说明。

这个任务用一块GTX 970的GPU训练的话大概要超过5个小时才能有结果,所以用CPU的话还是先死了心吧……

(9) 网络模型

训练的结果我们先放到一边,先来看一下Decoder网络的结构。毕竟对于Decoder来说,输入,输出都确定了,现在就是还没有明确网络结构和损失函数。

打开train.py文件,这个文件非常短,只有205行。

程序正文只有两行,就是:

opt = opts.parse_opt()

train(opt)

也就是说核心内容都是在train这个方法中了。

从第100行到118行,我们可以看到非常熟悉的结构,就是数据的读入,模型定义,损失函数。数据读入不用太在意,这是一个几乎是纯工程的部分,稍微有一点编程素养的同学很快就能看明白。而这里看着容易犯晕的可能是113行的loss和117行train_loss。

113行这里的model其实是做了正向传播的,那么model的定义在前面train.py执行的时候我们给了一个值“--caption_modelshow_tell”——使用show_tell模型,这个模型的定义在models的ShowTellModel.py。

Ps.你要想看明白这个参数是怎么传进去的,就需要看opts.py文件,这里定义了所有参数的名字和初始值。

我们继续来看show_tell模型的定义,从14行到33行:

rnn_type默认是LSTM,

rnn_size默认是512个,

num_layers默认是1,

seq_length默认是在dataloader.py中动态得到的

27行定义了一个全连接层,输入为fc_feat的大小,就是前面我们说的Resnet倒数第二层输出的FeatureMap,输出是LSTM输入的尺寸input_encoding_size。等于在这一层又做了一层encoding,而且这个系数是需要靠训练得到的。

28行这里卖弄了一个小技巧,但是简单说就是定义了一个LSTM网络,后面都是LSTM的参数。

29行定义的一个Word Embedding。大小是词汇表大小+1,input_encoding_size默认是512。

这里需要插入讲一下Word Embedding的概念了。通常来说,一个词汇要想数字化就可以使用独热编码。这里有一个前提假设,就是词汇和词汇之间是线性非相关的,即不可能有一个词汇通过倍数与另一词汇的倍数加减得到其它词汇。这种假设有它的合理性,不过它的局限性也是非常明显。我们在小语料范围内,比如一个二分类的图片库“猫”、“狗”分类这样的问题中,“猫”定义成[1,0],狗定义成[0,1]。这种情况下,猫怎么都不可能通过线性变换成为狗,这也是确实的情况。然而语料一旦扩大,我们刚才这种建模的方式就有问题了。尤其是在NLP的范畴中,会有大量的近义词、反义词和同义词关系,如果仍旧武断地把它们量化成独热向量,其实是在一定程度上误导语言模型,让它不能理解词汇之间真实存在的关系。例如我需要用一个向量来表示“直升机”,希望用另一个向量来表示“直升飞机”,这在大量的语料环境是完全可能碰到的,而且这两个词理论上来说我确实应该用一个相同的向量来表示才比较合理,这用独热编码是做不到的。再比如一个词“国王”的向量和一个词“男人”的向量,它们两者的差,我们想想看,是不是应该跟“王后”和“女人”的差相近呢?或者在IT类型的文章中“小米”和“苹果”之间的距离就比较近,而在农业类型的文章中“小米”和“大米”会更近一些,“苹果”和“梨”会更近一些(这里的近指的是空间欧几里得距离)。这就是Word Embedding的一些基本属性。

基于上述这样的理由,Word Embedding才应运而生。我们这样理解就好了:在一个相对确定的语料环境中,词汇之间是有一定客观关系存在的,就像我刚才说的那样的关系。而如果想要得到这样的关系就需要把原本做了独热化的词汇表通过某种训练的方式降维成为连续维度上的低维向量。而且这个过程是不需要人来做特殊标记的,因为上下文关系的限制本身就在相当大的程度上已经表明了它们的属性。例如“我明天去北京”,“我明天去天津”,如果分词不发生问题的情况下,计算机会比较容易得到一个概念,北京和天津在某些范畴上是近义词;而“小明去吃饭了”“我们去吃饭了”这样的句子在一定程度上暗示了小明和我们都有近似的属性(都是人)。这些关系通过海量语料的Word Embedding是可以让计算机得到一个比较可信的词汇向量表的,就是把每个词汇映射到连续空间上的样子,大概是这种感觉:

苹果:[0.8, 0.7, 0.2, -0.2]

梨:[0.78, 0.8, 0.3, -0.1]

……

一般来说,Word Embedding后的词汇都会是上百维的。这些维度人虽然没办法看懂——因为每个维度并非人所标注,所以没有具体解释,也很可能这些维度之间也非正交维度,但计算机通过语料的约束还是能够得到这样一个分布,而它们在对语义的理解问题中表现会更好。

注意nn.Embedding是Pytorch提供的一个功能模块,不需要再训练,可以直接使用并且满足比较“不特殊”的场合。如果你在项目中需要适应你工作需要的语聊场景,比如医疗、法律、IT等,那么就需要自己再训练一个更有针对性的Word Emedding模型了。

插入问题到此先告一段落,我们言归正传。

30行定义了一个全连接,输入是LSTM的输出,输出是词汇表大小+1。

31行是定义了一个DropOut比例,主要用来在全连接层预防过拟合。

到此为止我们来看一下这个部分的结构如何。

整个网络大概是这么个玩意儿,其实有点像用LSTM做MNIST的那个网络。

(10) 损失函数

正向传播的过程还是要先看train.py的113行,

model(fc_feats, att_feats, labels)

fc_feats和att_feats不必说了,看看labels,顺着102行的data =loader.get_batch('train')去找。

在dataloader.py的180行到191行

这里是以batch为单位,把一个batch中所有样本的fc_feats, att_feats, labels等这些属性都stack到一起(就像做夹心饼那样,一层一层叠起来)。

crit函数是调用的misc文件夹下面的utils.py中的39行到53行这个部分

这个部分的output就是forward的输出结果了,我的理解是对于输出为语言的这种模型来说,损失就是标准结果和拟合值的差距。这个损失看来看去还是input和target的差距。由于语料词汇都是Word Embedding过的,所以这个差距的值变化也是连续的,在梯度下降的过程中会有比较好的结果。

view是用来合并多行Tensor成为一行的方法,gather是沿着一个轴来合并Tensor的方法。

4、评估测试

在训练完成后调用eval.py命令进行评估测试

python eval.py --model log_st/model.pth--image_folder ~/imageroot/val2014/ --num_images 10 --infos_pathlog_st/infos_st.pkl

在测试的过程中程序会把图片拷贝到vis目录下面去,可以用来做可视化。

5、 可视化

cd vis

python -m http.server

然后就可以在浏览器中看到结果了http://localhost:8000

总体来说效果还是不错的,物体与关系识别还算准确。

这样的一个工程是可以做变化的,尤其是在安防领域,或者复杂场景识别领域中都可以考虑使用这样的工程的变种。

如果你在新媒体平台上的话,可以考虑用这种模型来做识别“精彩片段”、“最佳入球”等等对内容需要提取和一定程度理解的应用。

好,这次的分享就到此结束。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-05-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 奇点 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档