TensorFlow 雖然是目前最為流行的神經網路框架,卻以「難於上手」著稱(Jeff Dean:怪我咯)。有些時候,我們需要簡明扼要的程式碼來指點迷津。最近,來自 NCsoft AI 研究部門的 Junho Kim 就放出了一份這樣的 TensorFlow 程式碼集。它類似於一個迷你版的 Keras,只不過因為其簡單性,原始碼要好讀得多。
專案連結:https://github.com/taki0112/Tensorflow-Cookbook
在這個專案中,作者重點突出這是一份易於使用的 TensorFlow 程式碼集,它包括常見的正則化、卷積運算和架構模組等程式碼。實際上,在我們搭建自己的模型或系統時,複製並貼上這些程式碼就行了。它們以規範的形式定義不同的功能模組,因此只要修改少量引數與程式碼,它們就能完美地融入到我們專案中。
目前該專案包含一般深度學習架構所需要的程式碼,例如初始化和正則化、各種卷積運算、基本網路架構與模組、損失函式和其它資料預處理過程。此外,作者還特別增加了對 GAN 的支援,這主要體現在損失函式上,其中生成器損失和判別器損失可以使用推土機距離、最小二乘距離和 KL 散度等。
使用方法
使用方法其實有兩種,首先我們可以複製貼上程式碼,這樣對於模組的定製化非常有利。其次我們可以直接像使用 API 那樣呼叫操作與模組,這種方法會使模型顯得非常簡潔,而且匯入的原始碼也通俗易懂。首先對於第二種直接匯入的方法,我們可以從 ops.py 和 utils.py 檔案分別匯入模型運算部分與影象預處理過程。
from ops import *
from utils import *
from ops import convx = conv(x, channels=64, kernel=3, stride=2, pad=1, pad_type='reflect', use_bias=True, sn=True, scope='conv')複製程式碼
而對於第一種複製貼上,我們可能會根據實際修改一些引數與結構,但這要比從頭寫簡單多了。如下所示,對於一般的神經網路,它會採用如下結構模板:
def network(x, is_training=True, reuse=False, scope="network"): with tf.variable_scope(scope, reuse=reuse): x = conv(...) ...return logit複製程式碼
其實深度神經網路就像一塊塊積木,我們按照上面的模板把 ops.py 中不同的模組堆疊起來,最終就能得到完整的前向傳播過程。
程式碼集目錄
專案頁面:www.notion.so/Simple-Tens…
目前整個專案包含 20 種程式碼塊,它們可用於快速搭建深度學習模型:
程式碼示例
如下主要介紹幾段程式碼示例,包括最常見的卷積操作和殘差模組等。每一項程式碼示例都能採用 API 式的呼叫或複製貼上,所以它們不只能快速使用,學習各種操作的實現方法也是很好的資源。
卷積
卷積的原理相信大家都很熟悉,那就直接看呼叫程式碼吧:
x = conv(x, channels=64, kernel=3, stride=2, pad=1, pad_type='reflect', use_bias=True, sn=True, scope='conv')複製程式碼
如下所示為實現以上 API 的程式碼,相比於直接使用 padding='SAME',瞭解如何手給影象 padding 零也是很好的。此外,這一段程式碼嵌入了譜歸一化(spectral_normalization/sn),甚至我們可以擷取這一小部分嵌入到自己的程式碼中。
# padding='SAME' ======> pad = ceil[ (kernel - stride) / 2 ]def conv(x, channels, kernel=4, stride=2, pad=0, pad_type='zero', use_bias=True, sn=False, scope='conv_0'): with tf.variable_scope(scope): if pad > 0: h = x.get_shape().as_list()[1] if h % stride == 0: pad = pad * 2 else: pad = max(kernel - (h % stride), 0) pad_top = pad // 2 pad_bottom = pad - pad_top pad_left = pad // 2 pad_right = pad - pad_left if pad_type == 'zero': x = tf.pad(x, [[0, 0], [pad_top, pad_bottom], [pad_left, pad_right], [0, 0]]) if pad_type == 'reflect': x = tf.pad(x, [[0, 0], [pad_top, pad_bottom], [pad_left, pad_right], [0, 0]], mode='REFLECT') if sn: w = tf.get_variable("kernel", shape=[kernel, kernel, x.get_shape()[-1], channels], initializer=weight_init, regularizer=weight_regularizer) x = tf.nn.conv2d(input=x, filter=spectral_norm(w), strides=[1, stride, stride, 1], padding='VALID') if use_bias: bias = tf.get_variable("bias", [channels], initializer=tf.constant_initializer(0.0)) x = tf.nn.bias_add(x, bias) else: x = tf.layers.conv2d(inputs=x, filters=channels, kernel_size=kernel, kernel_initializer=weight_init, kernel_regularizer=weight_regularizer, strides=stride, use_bias=use_bias) return x複製程式碼
部分卷積(Partial Convolution)
部分卷積是英偉達為影象修復引入的卷積運算,它使模型能夠修復任意非中心、不規則的區域。在論文 Image Inpainting for Irregular Holes Using Partial Convolutions 中,實現部分卷積是非常關鍵的,如下展示了簡單的呼叫過程:
x = partial_conv(x, channels=64, kernel=3, stride=2, use_bias=True, padding='SAME', sn=True, scope='partial_conv') 複製程式碼
讀者可根據以下定義 PConv 的程式碼瞭解具體實現資訊:
def partial_conv(x, channels, kernel=3, stride=2, use_bias=True, padding='SAME', sn=False, scope='conv_0'): with tf.variable_scope(scope): if padding.lower() == 'SAME'.lower(): with tf.variable_scope('mask'): _, h, w, _ = x.get_shape().as_list() slide_window = kernel * kernel mask = tf.ones(shape=[1, h, w, 1]) update_mask = tf.layers.conv2d(mask, filters=1, kernel_size=kernel, kernel_initializer=tf.constant_initializer(1.0), strides=stride, padding=padding, use_bias=False, trainable=False) mask_ratio = slide_window / (update_mask + 1e-8) update_mask = tf.clip_by_value(update_mask, 0.0, 1.0) mask_ratio = mask_ratio * update_mask with tf.variable_scope('x'): if sn: w = tf.get_variable("kernel", shape=[kernel, kernel, x.get_shape()[-1], channels], initializer=weight_init, regularizer=weight_regularizer) x = tf.nn.conv2d(input=x, filter=spectral_norm(w), strides=[1, stride, stride, 1], padding=padding) else: x = tf.layers.conv2d(x, filters=channels, kernel_size=kernel, kernel_initializer=weight_init, kernel_regularizer=weight_regularizer, strides=stride, padding=padding, use_bias=False) x = x * mask_ratio if use_bias: bias = tf.get_variable("bias", [channels], initializer=tf.constant_initializer(0.0)) x = tf.nn.bias_add(x, bias) x = x * update_maskelse: if sn: w = tf.get_variable("kernel", shape=[kernel, kernel, x.get_shape()[-1], channels], initializer=weight_init, regularizer=weight_regularizer) x = tf.nn.conv2d(input=x, filter=spectral_norm(w), strides=[1, stride, stride, 1], padding=padding) if use_bias: bias = tf.get_variable("bias", [channels], initializer=tf.constant_initializer(0.0)) x = tf.nn.bias_add(x, bias) else: x = tf.layers.conv2d(x, filters=channels, kernel_size=kernel, kernel_initializer=weight_init, kernel_regularizer=weight_regularizer, strides=stride, padding=padding, use_bias=use_bias) return x 複製程式碼
殘差模組
ResNet 最大的特點即解決了反向傳播過程中的梯度消失問題,因此它可以訓練非常深的網路而不用像 GoogLeNet 那樣在中間新增分類網路以提供額外的梯度。而 ResNet 是由殘差模組堆疊起來的,一般根據需要可以定義幾種不同的殘差模組:
x = resblock(x, channels=64, is_training=is_training, use_bias=True, sn=True, scope='residual_block')x = resblock_down(x, channels=64, is_training=is_training, use_bias=True, sn=True, scope='residual_block_down')x = resblock_up(x, channels=64, is_training=is_training, use_bias=True, sn=True, scope='residual_block_up')複製程式碼
如上展示了三種殘差模組,其中 down 表示降取樣,輸入特徵圖的長寬都會減半;而 up 表示升取樣,輸入特徵圖的長寬都會加倍。在每一個殘差模組上,殘差連線會將該模組的輸入與輸出直接相加。因此在反向傳播中,根據殘差連線傳遞的梯度就可以不經過殘差模組內部的多個卷積層,因而能為前一層保留足夠的梯度資訊。
如下簡單定義了一般的 resblock 和採用升取樣的 resblock_up,因為它們呼叫的 conv()、deconv() 和 batch_norm() 等函式都是前面定義的不同計算模組,因此整體上程式碼看起來非常簡潔。
def resblock(x_init, channels, use_bias=True, is_training=True, sn=False, scope='resblock'): with tf.variable_scope(scope): with tf.variable_scope('res1'): x = conv(x_init, channels, kernel=3, stride=1, pad=1, use_bias=use_bias, sn=sn) x = batch_norm(x, is_training) x = relu(x) with tf.variable_scope('res2'): x = conv(x, channels, kernel=3, stride=1, pad=1, use_bias=use_bias, sn=sn) x = batch_norm(x, is_training) return x + x_initdef resblock_up(x_init, channels, use_bias=True, is_training=True, sn=False, scope='resblock_up'): with tf.variable_scope(scope): with tf.variable_scope('res1'): x = deconv(x_init, channels, kernel=3, stride=2, use_bias=use_bias, sn=sn) x = batch_norm(x, is_training) x = relu(x) with tf.variable_scope('res2') : x = deconv(x, channels, kernel=3, stride=1, use_bias=use_bias, sn=sn) x = batch_norm(x, is_training) with tf.variable_scope('skip') : x_init = deconv(x_init, channels, kernel=3, stride=2, use_bias=use_bias, sn=sn)複製程式碼
這裡只展示了三種功能塊的程式碼實現,可能我們會感覺該專案類似於一個迷你的 Keras。但因為這個專案實現的操作都比較簡單常見,因此原始碼讀起來會比 Keras 之類的大型庫簡單地多,這對於嵌入使用還是學習都更有優勢。