1. 程式人生 > >tensorflow2caffe(3) : 如何將tensorflow框架下訓練得到的權重轉化為caffe框架下的權重引數

tensorflow2caffe(3) : 如何將tensorflow框架下訓練得到的權重轉化為caffe框架下的權重引數

   在前兩期專欄tensorflow2caffe(1)tensorflow2caffe(2)中,筆者向大家介紹了caffemodel檔案型別下的引數架構和如何取出tensorflow框架下訓練引數。在本期中,筆者將向大家闡述,如何去將tensorflow框架下訓練得到的引數轉化為caffe框架下的規定格式的引數。首先,我們來捋一捋目前我們手裡面已經有了哪些東西:

1. 我們有自己的tensorflow訓練程式,也就是說我們知道訓練的網路架構。

2. 我們能夠得到tensorflow架構訓練得到的引數,並且我們知道我們的主要目的是得到一個caffemodel。

   那麼,請讀者朋友們想一想,我們現在還缺少什麼東西呢?

   要得到以上問題的答案,不妨思考如下這個問題,當我們使用caffe框架訓練模型完畢後,需要測試這個模型,我們必須的東西什麼?首先我們需要一個caffemodel,其次,在模型測試的時候,我們需要一個.prototxt檔案,該檔案記錄了網路前傳的邏輯順序。

   寫到這裡,知其然不如知其所以然,筆者不妨多說兩句。各位讀者朋友知道,當使用caffe框架訓練模型的時候,我們會使用一個prototxt檔案,姑且就叫他train.prototxt吧。那麼,在測試模型的時候,我們同樣使用了一個prototxt檔案,姑且將該檔案稱為test.prototxt。那麼,如何將train.prototxt、訓練得到的caffemodel檔案還有test.prototxt檔案關聯起來呢?答案是這樣的:caffemodel裡面包含了絕大部分train.prototxt的內容,就如

tensorflow2caffe(1)中所述。為什麼要這麼做,是因為train.prototxt除了約定了訓練網路架構與引數配置,更重要的是規定了鍵名,這個鍵名就是layer中的"name"引數,而該鍵名也會記錄在caffemodel中。在我們訓練完畢模型並使用test.prototxt結合caffemodel對模型進行測試時,相當於是根據test.prortotxt中的layer的"name"引數去取得鍵名,然後根據這個鍵名在caffemodel中取得引數,然後才能進行網路的前向傳播。到這裡,請讀者朋友們明白,test.prototxt是根據鍵名去caffemodel中取引數的,也就是說,如果提供的鍵名在caffemodel中找尋不到,那麼也就無從取值。這其實和我們使用caffe框架訓練模型時需要去finetune成熟模型的部分層的引數,於是我們就將我們的模型中需要finetune的layer的"name"引數改成finetune的caffemodel中對應layer的"name"一樣是同一個道理。

   言歸正傳,經過上面一段話的闡述,我們明白了,我們目前還缺少什麼東西。

(1) 我們需要一個test.prototxt。

(2) 我們需要將tensorflow訓練出來的引數轉化成文字,並且寫在test.prototxt裡面。

   首先,對於(1),筆者想說的是,在撰寫test.prototxt的時候,網路架構應該按照tensorflow訓練程式的網路架構來。也就是說,在寫作test.prototxt的時候,需要對tensorflow框架下面的訓練網路架構相當熟悉,並且明瞭tensorflow和caffe下面的框架協議規範。舉個栗子,在寫tensorflow卷積層時候,有很多讀者朋友可能會使用padding = "SAME" 這個引數,可是在caffe,沒有那麼智慧的操作,因此在寫test.prototxt框架下面卷積層的定義引數的時候,需要人為地去pad,再比如說,tensorflow下面的卷積實現的時候有時沒有在權重中加上bias,只有weight,那麼在撰寫test.prototxt的時候,就需要在該卷積層convolution_param的大括號中,加上"bias_term: false"的定義,這就需要對網路中資料流向和資料維度有相當程度的瞭解。除了這一點,還需要讀者朋友們注意的是,需要按需去重寫某些前傳層。比如說,tensorflow下面實現了一個啟用函式

def lrelu(x, leak=0.2, name = "lrelu"):
    return tf.maximum(x, leak*x)


   這種函式caffe官方是沒有定義的,也就是說,需要讀者朋友們自己去寫作caffe框架下的前傳程式碼(不需反傳函式)。並且,對於tensorflow和caffe兩個框架,對於某些層的實現機制是不一樣的。也就是說,在進行這一步的時候,請大家務必對訓練程式碼和caffe程式設計瞭解深刻,這樣才能夠在caffe框架下實現tensorflow的某些自定義層的邏輯。最後,形成一個完整的test.prototxt。

   接下來,筆者詳細地介紹一下(2)。

   在tensorflow2caffe(2)中,我們已經能夠打印出tensorflow下訓練得到的權重引數的名字了,也就是說可以得到權重。以卷積層為例,tesorflow框架下卷積層的權重shape是[kernel_height, kernel_width, input_channels, output_channels](相反,反捲積層的權重shape是[kernel_height, kernel_width, output_channels, input_channels])。

   那麼,caffe下面的引數規格在哪裡定義的呢?筆者提醒讀者朋友們,在tensorflow2caffe(1)中筆者有貼出讀出的caffemodel轉化得到的文字檔案截圖,讀者朋友們仔細觀察可以發現,caffemodel在每一個layer中,記錄引數的blobs大括號尾部,有一個shape屬性,裡面的dims就記錄了caffe框架下的引數格式,在caffe框架中,卷積層權重引數shape是[output_channels, input_channels, kernel_height, kernel_width](相反地,反捲積層權重引數是[input_channels, output_channels, kernel_height, kernel_width])。因此,我們需要將引數的維度加以變換。

   到這裡的時候,筆者的困惑就來了,舉個栗子,某個卷積層輸入channel是128,輸出channel是256,二維卷積核長寬都是4,沒有bias引數,那麼,該卷積層的權重引數數量是多少呢?

   答案是128×256×4×4 = 524288個,這個時候,用什麼函式或者工具能夠輕而易舉地轉化權重引數的維度呢?

   筆者最開始在解決這個問題的時候也一籌莫展,甚至使用c語言進行過陣列處理,可是事實證明,這樣處理的方式是低效的。那麼如何取得引數維度轉化時的高效率呢?筆者的同事@feiyang想出瞭解決方案,使用numpy的swapaxes函式,幾行程式碼就可解決問題。

   筆者使用的程式碼如下:

#!/usr/bin/python

import tensorflow as tf
import numpy as np

with tf.Session() as sess:
	new_saver = tf.train.import_meta_graph('.model.meta')
	for var in tf.trainable_variables():
		print var.name
	new_saver.restore(sess, tf.train.latest_checkpoint('./checkpoint_dir/'))
	all_vars = tf.trainable_variables()
	for v in all_vars:
		name = v.name
		fname = name + '.prototxt'
		fname = fname.replace('/','_')
		print fname
		v_4d = np.array(sess.run(v))
		if v_4d.ndim == 4:
			#v_4d.shape [ H, W, I, O ]		
			v_4d = np.swapaxes(v_4d, 0, 2) # swap H, I
			v_4d = np.swapaxes(v_4d, 1, 3) # swap W, O
			v_4d = np.swapaxes(v_4d, 0, 1) # swap I, O
			#v_4d.shape [ O, I, H, W ]
			f = open(fname, 'w')
			vshape = v_4d.shape[:]
			v_1d = v_4d.reshape(v_4d.shape[0]*v_4d.shape[1]*v_4d.shape[2]*v_4d.shape[3])
        		f.write('  blobs {\n')
			for vv in v_1d:
				f.write('    data: %8f' % vv)
				f.write('\n')
			f.write('    shape {\n')
			for s in vshape:
				f.write('      dim: ' + str(s))#print dims
				f.write('\n')
			f.write('    }\n')
			f.write('  }\n')
		elif v_4d.ndim == 1 :#do not swap
			f = open(fname, 'w')
			f.write('  blobs {\n')
			for vv in v_4d:
				f.write('    data: %.8f' % vv)
				f.write('\n')
			f.write('    shape {\n')
			f.write('      dim: ' + str(v_4d.shape[0]))#print dims
			f.write('\n')
			f.write('    }\n')
			f.write('  }\n')
		f.close()


   首先,程式碼的上半部分和tensorflow2caffe(2)中的一樣,至於程式碼的下半部分,就涉及到引數維度的轉換了,筆者將每一層對應的權重引數逐一取出來,並相應地新建了.prototxt檔案並按照caffemodel下面的引數格式寫入了檔案中。

   值的讀者朋友們注意的是,由於筆者模型下面只涉及到四維(卷積層,反捲積層)和一維(batch_norm的乘數(scale),偏置(offset))的引數,因此筆者在寫引數檔案中只對這兩類引數做了處理,讀者朋友們在使用的時候可以按需作出處理。

   執行一下上述程式。

   可以看到,轉化為caffe框架格式的各層權重引數檔案已經儲存在了路徑下:

   隨便點開一個卷積層的引數檔案,頭尾部分如下兩圖所示:

   讀者朋友們可以看到,權重引數的shape是不是變成了caffe框架下的[output_channels, input_channels, kernel_height, kernel_width]了呢?

   現在,我們有了一個test.prototxt檔案,還有了各個層的引數,那麼,下面就將我們轉化得到的引數寫入test.prototxt檔案就好了。那麼,引數應該寫到層裡面的什麼地方呢?很簡單,直接將我們得到的引數檔案寫入對應層的大括號內就好辣!ヾ(✿゚▽゚)ノ,如下程式碼示意:

layer {
	name: "conv_layer_name"
	type: "Convolution"
	bottom: "bottom_blob"
	top: "top_blob"
	param { lr_mult: ... }
	convolution_param {
		num_output: output_dims
		kernel_size: kernel_size
		pad: padding_size
		stride: stride
		bias_term: false
	}
	#add params
        blobs: {
          data: ...
          ...
          shape {
            dim: ...
            dim: ...
            dim: ...
            dim: ...
          }
        }
}


   筆者在這裡教大家一個小技巧,我們既然得到了記錄各層引數的眾多.prototxt檔案,又有一個test.prototxt,不如我們將test.prototxt按照新增斷點拆成若干部分,然後製作一個.sh指令碼,就可以將各層引數新增進test.prototxt檔案中了哦,姑且稱這個新增過權重引數的檔案為model.prototxt檔案吧。

   筆者就是將test.prototxt按照拼接斷點拆開:

   然後再將上圖中的檔案和轉化得到的.prototxt格式的眾多引數檔案放在同一目錄下,並且使用一個index.txt檔案從上到下記錄了拼接順序:

   然後使用一個名為ss.sh的指令碼檔案作拼接:

#!/bin/bash

cat index.txt |while read line
do
cat $line >>model.prototxt
done


   然後我們執行ss.sh檔案:

   可以看到生成了model.prototxt檔案。那麼,這個檔案有多大呢?

   大家可以看到,該檔案是相當大的,因此,強烈推薦大家使用指令碼對檔案進行拼接得到最終的模型檔案。

   可是,最終的模型檔案有什麼用呢?最終的模型檔案將被轉化為.caffemodel的模型檔案並在測試程式中被呼叫。那麼,如何將最終的.ptototxt模型檔案轉化為.caffemodel檔案呢?預知後事如何,請看下篇分解!

   總的來說,在tensorflow2caffe框架轉換的過程中,本篇描述的是最難的部分,也是筆者闡述相當認真的一部分,希望能對各位讀者朋友有幫助和有啟發,對於部落格中的疏漏,萬望各位讀者朋友在評論區指出,筆者不勝感激!

   對於筆者的每一篇部落格,筆者都是記錄與闡述的科研和專案中的乾貨,筆者也會盡力做到常常更新,如果各位讀者朋友們覺得筆者的部落格對大家有幫助,訂閱是歡迎的,廣而告之更是歡迎的!

   歡迎閱讀筆者後續部落格,各位讀者朋友的支援與鼓勵是我最大的動力!

written by jiong

功崇惟志,業廣惟勤