neuraltalk2-代码解析(1)
原文地址:http://blog.csdn.net/qq_30133053/article/details/52328383
第一篇,我先解析下neuraltalk2代码中的utils.lua,netutils.lua工具包,和DataLoader.lua数据加载文件。
首先声明:代码不是原创,而是转载,获得代码在之前的博客给出了链接。
utils.lua
这个工具文件由utils.getopt(opt,key,default_value),utils.read_json(path),
utils.write_json(path,j),utils.dict_average(dicts),
utils.count_keys(t),utils.average_values(t)这六个方法构成,有些方法我都直接明白,所以有些具体细节就不多叙述。
-
第一点
- 大家可以了解一个什么是json格式,百度百科给的介绍还是十分不错的,在实际代码其实只用到了cjson包中两个函数,分别为encode_sparse_array()与encode(),链接CJSON,encode_sparse_array()简单来说就是编码稀疏数组,即数组中的缺失元素用nil值来代替,encode()是将json格式的txt转换为lua可识别的table形式。
-
utils.getopt(opt, key, default_value)
- 简单叙述下,这个方法是查询在opt中有木有key键值所对应的值opt[key],如果有则返回opt[key],如果没有则返回default_value,若传入的default_value为空,这时候getopt方法报错,输出“error: required key ’ .. key .. ’ was not provided in an opt.”
-
utils.read_json(path)
-
这个方法根据path路径,读取一个json格式的文件,返回一个lua可识别的table结构。
- utils.write_json(path,j)
-
这个方法是根据path路径,现将j转换为一个稀疏的数组,在将j存储。
- utils.dict_average(dicts)
-首先了解一下这个方法作用的variable结构,dicts是一个链表,链表中的每个元素数为k:v对的table,并且该方法的前提是所有dicts[n]的k值相同,所以该方法返回dict table中的每个键值,所对应的值value为,原dicts链表中每个元素table,所对应相同键值的平均值。 -
function utils.dict_average(dicts) local dict = {} local n = 0 --遍历链表中的每个元素(类型table) for i,d in pairs(dicts) do --遍历每个table元素,并且将值加到新建的dict变量中,如果键值k对应的value存在 for k,v in pairs(d) do if dict[k] == nil then dict[k] = 0 end dict[k] = dict[k] + v end --记录链表中元素的数量 n=n+1 end --遍历新variable(类型table),将每个键值对应的value取average for k,v in pairs(dict) do dict[k] = dict[k] / n -- produce the average end return dict end
- utils.dict_average(dicts)
-
-
utils.count_keys(t)
- variable t(类型table)这个方法返回t中k:V对的数量 ,说实话不知道这个方法用来干嘛的(@.@),可以解释为K神讨厌#这个operator
-
utils.average_values(t)
- 输入variable t(类型table)这个方法返回t中所有value和的平均值
-
net_utils.lua
- 这个网络工具大致由两部分组成,一部分为net_utils网络工具,另一部分为nn.FeatExpander这个类(这个类继承于nn.module,对nn.module不熟悉的同学可以看我其他博客,有可能暂时没发,以后会不上对nn.module的分析与实例解析)
nn.FeatExpande
- 这个类继承与nn.module,因此拥有nn.module的一些特性。继承于module类都有两个接口{{input},{output}},这个类的功能为将input即输入张量,对其扩充n倍,形成输出的output张量,所以这个类不涉及任何的神经网络参数的训练。
- layer_init(n)这个方法用来初始化父类,并且初始化FeatExpander的初始参数n,n为input扩充的倍数。layer:updataOutput(input) 这个方法是在继承nn.module类,建议重写的方法,这个方法定义了如何更新输出向量,即这个方法的最终返回结果为输出向量,具体解析等我后面粘的代码。layer:updataGradInput(input,gradOutput)这个方法也是重写了nn.module中的方法,这个方法这用来确定当误差传导到output时,如何将误差传导到input层,所以这个方法返回的是input的梯度。具体详解将在后面粘出。
-
function layer:updateOutput(input) --若layer中n为1,则并不需要扩充,相当与做一个空操作 if self.n == 1 then self.output = input; return self.output end -- act as a noop for efficiency -- simply expands out the features. Performs a copy information -- 确认输入张量是否为2维的 assert(input:nDimension() == 2) -- 得到第二维的长度 local d = input:size(2) --重新设定输出向量的大小,第一维的大小为原输入数据第一维的大小乘以扩充倍数,第二维的大小不做任何改变 self.output:resize(input:size(1)*self.n, d) --将每组数据足一拷贝,是以第一维来分组 for k=1,input:size(1) do --这里的K也可以看作是指定的是第k行数据,j为第原input第k行数据在output中所处的第j行 local j = (k-1)*self.n+1 --值得注意的是这里的数据是成块的,即j行与j+self.n行之间都是相同的数据,expand()函数是不会分配新的内存,即其实扩展数据是不存在。具体函数详解可去官方的帮助去找 self.output[{ {j,j+self.n-1} }] = input[{ {k,k}, {} }]:expand(self.n, d) -- copy over end return self.output end
function layer:updateGradInput(input, gradOutput) --n为1,空操作 if self.n == 1 then self.gradInput = gradOutput; return self.gradInput end -- act as noop for efficiency -- add up the gradients for each block of expanded features --重新设定self.gradInput的大小,按照input的size self.gradInput:resizeAs(input) --获得input的第二维的大小(应该说范数,但是我并不是学数学滴^_^) local d = input:size(2) --以input:size(1)为循环的条件,是因为input:size(i)表示不同的数据有多少行,可以很方便的检索数据 for k=1,input:size(1) do --j为在input中第k行数据,在output中的第j行,注意gradOutput与output是同样的维度大小 local j = (k-1)*self.n+1 --对每一列进行求和操作,即相同数据对应的梯度求和 self.gradInput[k] = torch.sum(gradOutput[{ {j,j+self.n-1} }], 1) end return self.gradInput end
net_utils.build_cnn(cnn,opt)
- 这个方法根据输入的参数opt与从caffe平台取得的cnn来进行构造cnn网络,这个cnn模型为VGG-16,详解在下面贴出。
function net_utils.build_cnn(cnn, opt) --utils.getopt(a,b,c) 其中c为默认参数 --layer_num为从caffe中取得cnn的层数 local layer_num = utils.getopt(opt, 'layer_num', 38) --backend为训练的方式,这里选定为GPU local backend = utils.getopt(opt, 'backend', 'cudnn') --encoding_size为最后cnn网路应该输出向量的长度 local encoding_size = utils.getopt(opt, 'encoding_size', 512) --后端的设定若backend为cudnn则导入cudnn包,支持GPU运算,若为nn则导入nn包,支持CPU运算 if backend == 'cudnn' then require 'cudnn' backend = cudnn elseif backend == 'nn' then require 'nn' backend = nn else error(string.format('Unrecognized backend "%s"', backend)) end -- copy over the first layer_num layers of the CNN --nn.Sequential()是容器类,队列型容器,如果对容器类不熟悉的同学,可以看其他博客,可能我还没写(^_^)!,有机会补上 local cnn_part = nn.Sequential() for i = 1, layer_num do --获得每层的module local layer = cnn:get(i) if i == 1 then -- convert kernels in first conv layer into RGB format instead of BGR, -- which is the order in which it was trained in Caffe --将BGR形式的参数形式转换为RGB格式的参数,因为在caffe中训练图片的颜色通道为BGR。 --Clone参数,注意这里的clone相当与C语言中,直接将指针的地址复制,也就是weight和w中的参数指向的是同一地址,并不是深拷贝。 local w = layer.weight:clone() -- swap weights to R and B channels print('converting first layer conv filters from BGR to RGB...') --从这里跟大家分析一下这个cnn网络参数的格式,参数都是4维张量,第一维的大小为batch_size,第二维为颜色通道大小为3,第三维和第四维都是图片的size。 layer.weight[{ {}, 1, {}, {} }]:copy(w[{ {}, 3, {}, {} }]) layer.weight[{ {}, 3, {}, {} }]:copy(w[{ {}, 1, {}, {} }]) end --添加网络层 cnn_part:add(layer) end --这时已经得到的是cnn的最后一层 --在最后一层添加到encoding_size维度的转换,从这里可以看出VGG-16最后一层网络的维数大小为4096,这与论文比较符合。 cnn_part:add(nn.Linear(4096,encoding_size)) --添加非线性层,这里用的是ReLU非线性函数 --这里backend为cunn字符串 cnn_part:add(backend.ReLU(true)) return cnn_part end
- 这个方法是对输入图像进行预处理过程,因为VGG-16网络是写死的,其只使用与width和height为224大小的图片,所以如果输入图片超过了这个size,就必须经过一定的预处理,详细解析在下面贴出
--提取batchsize长度的images并且进行预处理,这里还是跟大家说明一下数据的格式,imgs为维数为4的张量,第一维大小为batch_size,第二维的大小为3,代表三个颜色通道,第三维的大小为width,第四维的大小为height -- takes a batch of images and preprocesses them -- VGG-16 network is hardcoded, as is 224 as size to forward -- VGG-16 网络是写死的网络,224大小是网络初始层固定的大小 function net_utils.prepro(imgs, data_augment, on_gpu) --确认data_augment与on_gpu这两个参数是否输入正常 assert(data_augment ~= nil, 'pass this in. careful here.') assert(on_gpu ~= nil, 'pass this in. careful here.') --得到图片的高与宽 local h,w = imgs:size(3), imgs:size(4) local cnn_input_size = 224 -- cropping data augmentation, if needed -- 确认是否进行数据,样本的扩充 if h > cnn_input_size or w > cnn_input_size then local xoff, yoff if data_augment then --如果进行数据扩充,这图片中随机提取224大小的区域,我认为这个方法同一组数据不只调用一次,torch.random(a,b)是随机生成一个在a,b之间的整数,默认a为0。 xoff, yoff = torch.random(w-cnn_input_size), torch.random(h-cnn_input_size) else -- sample the center --如果不进行数据的扩充,则直接取中央的像素块 xoff, yoff = math.ceil((w-cnn_input_size)/2), math.ceil((h-cnn_input_size)/2) end -- crop. imgs = imgs[{ {}, {}, {yoff,yoff+cnn_input_size-1}, {xoff,xoff+cnn_input_size-1} }] end -- ship to gpu or convert from byte to float --转换数据格式 if on_gpu then imgs = imgs:cuda() else imgs = imgs:float() end -- lazily instantiate vgg_mean --其实本人在2016-8-26时并不熟悉VGG-16网络 if not net_utils.vgg_mean then net_utils.vgg_mean = torch.FloatTensor{123.68, 116.779, 103.939}:view(1,3,1,1) -- in RGB order end --typsAs()是按照imgs的格式重新返回一个tensor net_utils.vgg_mean = net_utils.vgg_mean:typeAs(imgs) -- a noop if the types match -- 根据VGG——mean将数据中心化 -- subtract vgg mean imgs:add(-1, net_utils.vgg_mean:expandAs(imgs)) --这个预处理过程,实际上是VGG-16去中值的过程 return imgs --返回经过处理之后的数据 end
-
net_utils.list_nngraph_modules(g)
- 这个方法不详解,g variable的类型是gModule,其返回的是nngraph模型的链表
-
net_utils.listModule(net)
- 这个方法也不详解,是将net结构以链表的形式返回
-
net_utils.sanitize_gradients(net),net_utils.unsanitize_gradients(net)
- 这两个方法相互对照,分别为清空梯度,恢复梯度
net_utils.decode_sequence(ix_to_word,seq)
-这个方法是用来解码的,两个输入参数ix_to_word,seq,分别代表者向量到字符串(英文字母的映射),和需要解码的序列,详解。
--[[ take a LongTensor of size DxN with elements 1..vocab_size+1 (where last dimension is END token), and decode it into table of raw text sentences. each column is a sequence. ix_to_word gives the mapping to strings, as a table --]] function net_utils.decode_sequence(ix_to_word, seq) --这里跟大家解析下D,N分别代表着什么,他们的实际意义是什么,N代表着序列的个数,通常为batch_size,D为seq_length local D,N = seq:size(1), seq:size(2) --这是要输出的文档 local out = {} for i=1,N do local txt = '' --遍历每个序列 for j=1,D do --取输入向量inputx local ix = seq[{j,i}] --将ix转换为字符输入ix_to_word映射中,得到真正的英文单词word local word = ix_to_word[tostring(ix)] --如果word不存在,代表已经到了序列末尾,执行结束代码 if not word then break end -- END token, likely. Or null token --..字符串连接 --K神的格式真是讲究,然道是处女座!_!, --每两个词之间用空格隔开 if j >= 2 then txt = txt .. ' ' end txt = txt .. word end --将文本插入即将要输出的table table.insert(out, txt) end --out为全部文档 return out end
-
net_utils.clone_list(list)
- 复制链表,注意这里是深拷贝。
-
net_utils.language_eval(predicaitions,id)
- 这个方法用于测试预测结果,代码写得很逗
-
DataLoader.lua
- 这个文件有DataLoader这个类构成,这个类是用来加载数据。这个文件导入了hdf5工具包,有像我一样的新手可能问了,什么是hdf5包,这个包也是torch中用于数据处理的工具包(一点也不好笑),用来读取hdf5形式的文件。
-
DataLoader:_init(opt)
不多说,直接上。
function DataLoader:__init(opt) -- load the json file which contains additional information about the dataset print('DataLoader loading json file: ', opt.json_file) self.info = utils.read_json(opt.json_file) --ix_to_word是输入向量到词空间的一个映射 self.ix_to_word = self.info.ix_to_word --vocab_size标明词个数,也是维度的标记,最后一个词为END特殊词 self.vocab_size = utils.count_keys(self.ix_to_word) print('vocab size is ' .. self.vocab_size) -- open the hdf5 file print('DataLoader loading h5 file: ', opt.h5_file) self.h5_file = hdf5.open(opt.h5_file, 'r') -- extract image size from dataset --返回images各种维度的大小,想细究的同学可以去(https://github.com/deepmind/torch-hdf5/blob/master/luasrc/dataset.lua)学习,才疏学浅暂时还没看 --images_size[1]为图片数量,images_size[2]为通道数量,images_size[3],images_size[4]为图片的尺寸 local images_size = self.h5_file:read('/images'):dataspaceSize() assert(#images_size == 4, '/images should be a 4D tensor') assert(images_size[3] == images_size[4], 'width and height must match') self.num_images = images_size[1] self.num_channels = images_size[2] self.max_image_size = images_size[3] print(string.format('read %d images of size %dx%dx%d', self.num_images, self.num_channels, self.max_image_size, self.max_image_size)) -- load in the sequence data local seq_size = self.h5_file:read('/labels'):dataspaceSize() --seq_size[1]应为序列的数量,seq_size[2]应为序列的长度,即为seq_lenght self.seq_length = seq_size[2] print('max sequence length in data is ' .. self.seq_length) -- load the pointers in full to RAM (should be small enough) -- 注意这里获取的是所有序列的开始向量,与end向量的位置 self.label_start_ix = self.h5_file:read('/label_start_ix'):all() self.label_end_ix = self.h5_file:read('/label_end_ix'):all() -- separate out indexes for each of the provided splits self.split_ix = {} --这个是迭代器,用来index self.iterators = {} --self.info.images是json格式数据信息 for i,img in pairs(self.info.images) do --这里的img.split是image的标签分别为“train”,“valid”,"test" local split = img.split if not self.split_ix[split] then -- initialize new split self.split_ix[split] = {} self.iterators[split] = 1 end --将对应label的图片插入table中 table.insert(self.split_ix[split], i) end --输出图片信息 for k,v in pairs(self.split_ix) do print(string.format('assigned %d images to split %s', #v, k)) end end
-
resetIterator(split),getvocabsize(),getvocab(),getseqlength()
- 这几个函数就不多说了
-
DataLoader:getBatch(opt)
这个方法用来获得一个batch_size的数据,直接看解析
--[[ Split is a string identifier (e.g. train|val|test) Returns a batch of data: - X (N,3,H,W) containing the images - y (L,M) containing the captions as columns (which is better for contiguous memory during training) - info table of length N, containing additional information The data is iterated linearly in order. Iterators for any split can be reset manually with resetIterator() --]] function DataLoader:getBatch(opt) --split用来指定获得哪种数据,train|val|test local split = utils.getopt(opt, 'split') -- lets require that user passes this in, for safety --获得batch_szie local batch_size = utils.getopt(opt, 'batch_size', 5) -- how many images get returned at one time (to go through CNN) local seq_per_img = utils.getopt(opt, 'seq_per_img', 5) -- number of sequences to return per image --split_ix里面存的是imgs的索引 local split_ix = self.split_ix[split] assert(split_ix, 'split ' .. split .. ' not found.') --创建batch_img的初始张量 -- pick an index of the datapoint to load next local img_batch_raw = torch.ByteTensor(batch_size, 3, 256, 256) --创建label_batch的初始向量 local label_batch = torch.LongTensor(batch_size * seq_per_img, self.seq_length) --获得最大的索引值,为split_ix的最大数量,#操作符为去split_ix的长度 local max_index = #split_ix local wrapped = false local infos = {} for i=1,batch_size do local ri = self.iterators[split] -- get next index from iterator local ri_next = ri + 1 -- increment iterator --如果超过了最大索引,表示已经通过了一个轮换 if ri_next > max_index then ri_next = 1; wrapped = true end -- wrap back around --这是改变了self.iterators[split]中的迭代序号,为了方便下次去样本 self.iterators[split] = ri_next --获得图像的索引 ix = split_ix[ri] assert(ix ~= nil, 'bug: split ' .. split .. ' was accessed out of bounds with ' .. ri) -- fetch the image from h5 --img是一个4维的张量,第一维为1(因为提取的是{ix,ix},即单个图片),第二维为通道对应{1,self.num_channels},代表了三个通道,剩下两个维度为图片的大小 local img = self.h5_file:read('/images'):partial({ix,ix},{1,self.num_channels}, {1,self.max_image_size},{1,self.max_image_size}) --添加图片 img_batch_raw[i] = img -- fetch the sequence labels -- 首先获得ix所对应的序列的start与end序号,分别为ix1,ix2 local ix1 = self.label_start_ix[ix] local ix2 = self.label_end_ix[ix] -- 获得描述该图片语句的数量 local ncap = ix2 - ix1 + 1 -- number of captions available for this image assert(ncap > 0, 'an image does not have any label. this can be handled but right now isn\'t') local seq --查看num of caption是否满足刚开始设定的seq_per_img if ncap < seq_per_img then -- we need to subsample (with replacement) -- 如果数量过少则找部分样本代替 seq = torch.LongTensor(seq_per_img, self.seq_length) for q=1, seq_per_img do local ixl = torch.random(ix1,ix2) --这是随机提取的,注定有同样的标记可能被提取多遍 seq[{ {q,q} }] = self.h5_file:read('/labels'):partial({ixl, ixl}, {1,self.seq_length}) end else -- there is enough data to read a contiguous chunk, but subsample the chunk position -- captions数量足够,取连续的captions,但第一个caption是随机的 local ixl = torch.random(ix1, ix2 - seq_per_img + 1) -- generates integer in the range seq = self.h5_file:read('/labels'):partial({ixl, ixl+seq_per_img-1}, {1,self.seq_length}) end --il是在label_batch中第i号图片的第一个索引位置 local il = (i-1)*seq_per_img+1 --将seq储存到label_batch中 label_batch[{ {il,il+seq_per_img-1} }] = seq -- and record associated info as well local info_struct = {} info_struct.id = self.info.images[ix].id info_struct.file_path = self.info.images[ix].file_path table.insert(infos, info_struct) end local data = {} data.images = img_batch_raw --将1维,与2维交换 data.labels = label_batch:transpose(1,2):contiguous() -- note: make label sequences go down as columns data.bounds = {it_pos_now = self.iterators[split], it_max = #split_ix, wrapped = wrapped} data.infos = infos return data end
以上解析是对utils.lua,net_utils.lua,DataLoader.lua的解析,其他必要文件的解析也会逐渐不上。