1. 程式人生 > >基於卷積神經網路實現圖片風格的遷移 3

基於卷積神經網路實現圖片風格的遷移 3

實現圖片的風格轉換

一、實驗介紹

1.1 實驗內容

上一節課我們介紹了經典的CNN模型 VGG ,以及影象風格遷移演算法的基本原理。本節課我們將使用另外一個經典的模型 GoogLenet 來實現我們的專案(這是由於環境的限制,用 googlenet可以更快的完成我們的風格轉換),如果你完成了上節課的作業,那麼你應該基本瞭解了 GoogleNet 的原理。在本節實驗的演算法展示中,我希望你可以將程式碼與上節課所講解公式對照著看來幫助自己更好地理解,現在讓我們開始本節實驗,完成後你就可以親自動手實現任意圖片的風格轉換。

1.2 實驗知識點

  • style-transfer
  • 風格遷移演算法程式碼詳解
  • 實現圖片的任意風格轉換

1.3 實驗環境

  • python 2.7、pip、numpy
  • caffe
  • Xfce終端

1.4 實驗思路

我們可以先整理一下思路,從前面幾節課的學習我們可以知道訓練一個模型,我們需要準備:

  • 一個訓練好的神經網路官方參考的是Vgg16、Vgg19、Caffenet、Googlenet四類
  • 一張風格影象style image,用來計算它的風格representation
  • 一張內容影象content image,用來計算它的內容representation,
  • 一張噪聲影象result image,用來迭代優化一般都是拿Content內容圖來做,Caffe裡面預設也是拿內容圖來作為底圖
  • 一個loss函式,用來獲得loss
  • 一個求反傳梯度的計算圖,用來依據loss獲得梯度修改圖片 Caffe有明確的forward和backward,會幫我們自動計算

二、實驗步驟

2.1 style-transfer

2.1.1 簡介

caffe的官方完美的支援Python語言的相容,提供的介面就是pyCaffe

style-tansfer提供了將一個輸入影象的藝術風格轉移到另一個輸入影象的方法,即影象風格遷移。

在這個演算法中,神經網路操作由Caffe處理,使用numpy和scipy執行損耗最小化和其他雜項矩陣運算, L-BFGS用於最小化操作(關於L-BFGS)。

2.1.2 獲取原始碼

啟動終端

$ cd /home/shiyanlou
$ wget http://labfile.oss.aliyuncs.com/courses/861/style-transfer-master.zip
$ mv style-transfer-master style-transfer

2.2 核心程式碼

在本實驗中,所有的操作都在style.py 中完成。

首先讓我們來看一下 /home/shiyanlou/style-transfer/style.py 中的核心演算法:

2.2.1 矩陣計算

def _compute_reprs(net_in, net, layers_style, layers_content, gram_scale=1):
        """
        首先正向傳播計算出各層feature map,再將特徵矩陣儲存返回
        """
    (repr_s, repr_c) = ({}, {})
    net.blobs["data"].data[0] = net_in
    net.forward()

    for layer in set(layers_style)|set(layers_content):
        F = net.blobs[layer].data[0].copy()
        F.shape = (F.shape[0], -1)
        repr_c[layer] = F
        if layer in layers_style:
            repr_s[layer] = sgemm(gram_scale, F, F.T)

    return repr_s, repr_c

其中的網路權重的定義:

GOOGLENET_WEIGHTS = {"content": {"conv2/3x3": 2e-4,
                                 "inception_3a/output": 1-2e-4},
                     "style": {"conv1/7x7_s2": 0.2,
                               "conv2/3x3": 0.2,
                               "inception_3a/output": 0.2,
                               "inception_4a/output": 0.2,
                               "inception_5a/output": 0.2}}

內容用了2層,風格用了5層。我們可以用key()來獲取層名:

layers_style = weights["style"].keys()
layers_content = weights["content"].keys()

2.2.2 梯度處理

def _compute_style_grad(F, G, G_style, layer):
        """
        完成風格梯度以及loss的計算
        """
    (Fl, Gl) = (F[layer], G[layer])
    c = Fl.shape[0]**-2 * Fl.shape[1]**-2
    El = Gl - G_style[layer]
    loss = c/4 * (El**2).sum()
    grad = c * sgemm(1.0, El, Fl) * (Fl>0)

    return loss, grad

        """
        完成內容梯度以及loss的計算
        """
def _compute_content_grad(F, F_content, layer):
    Fl = F[layer]
    El = Fl - F_content[layer]
    loss = (El**2).sum() / 2
    grad = El * (Fl>0)

    return loss, grad

2.2.3 噪聲影象擬合

def _make_noise_input(self, init):
        """
        製造最開始的噪聲輸入,但是我們預設使用 `content` 作噪聲影象。我們就是在這個上面進行不斷的擬合
        """

        # 指定維度並在傅立葉域中建立網格
        dims = tuple(self.net.blobs["data"].data.shape[2:]) + \
               (self.net.blobs["data"].data.shape[1], )
        grid = np.mgrid[0:dims[0], 0:dims[1]]

        # 建立噪聲的頻率表示
        Sf = (grid[0] - (dims[0]-1)/2.0) ** 2 + \
             (grid[1] - (dims[1]-1)/2.0) ** 2
        Sf[np.where(Sf == 0)] = 1
        Sf = np.sqrt(Sf)
        Sf = np.dstack((Sf**int(init),)*dims[2])

        # 應用並使用 ifft 規範化
        ifft_kernel = np.cos(2*np.pi*np.random.randn(*dims)) + \
                      1j*np.sin(2*np.pi*np.random.randn(*dims))
        img_noise = np.abs(ifftn(Sf * ifft_kernel))
        img_noise -= img_noise.min()
        img_noise /= img_noise.max()

        # 預處理
        x0 = self.transformer.preprocess("data", img_noise)

        return x0

2.2.4 風格轉換

def transfer_style(self, img_style, img_content, length=512, ratio=1e5,
                       n_iter=512, init="-1", verbose=False, callback=None):


        # 假設卷積層的輸入是平方的
        orig_dim = min(self.net.blobs["data"].shape[2:])

        # 縮放影象
        scale = max(length / float(max(img_style.shape[:2])),
                    orig_dim / float(min(img_style.shape[:2])))
        img_style = rescale(img_style, STYLE_SCALE*scale)
        scale = max(length / float(max(img_content.shape[:2])),
                    orig_dim / float(min(img_content.shape[:2])))
        img_content = rescale(img_content, scale)

        # 計算表示風格的特徵矩陣
        self._rescale_net(img_style)
        layers = self.weights["style"].keys()
        net_in = self.transformer.preprocess("data", img_style)
        gram_scale = float(img_content.size)/img_style.size
        G_style = _compute_reprs(net_in, self.net, layers, [],
                                 gram_scale=1)[0]

        # 計算表示內容的特徵矩陣
        self._rescale_net(img_content)
        layers = self.weights["content"].keys()
        net_in = self.transformer.preprocess("data", img_content)
        F_content = _compute_reprs(net_in, self.net, [], layers)[1]

        # 噪聲影象輸入
        if isinstance(init, np.ndarray):
            img0 = self.transformer.preprocess("data", init)
        elif init == "content":
            img0 = self.transformer.preprocess("data", img_content)
        elif init == "mixed":
            img0 = 0.95*self.transformer.preprocess("data", img_content) + \
                   0.05*self.transformer.preprocess("data", img_style)
        else:
            img0 = self._make_noise_input(init)

        # 計算資料邊界
        data_min = -self.transformer.mean["data"][:,0,0]
        data_max = data_min + self.transformer.raw_scale["data"]
        data_bounds = [(data_min[0], data_max[0])]*(img0.size/3) + \
                      [(data_min[1], data_max[1])]*(img0.size/3) + \
                      [(data_min[2], data_max[2])]*(img0.size/3)

        # 引數優化
        # 使用L-BFGS-B演算法可以最小化 loss function 而且空間效率較高。
        grad_method = "L-BFGS-B"
        reprs = (G_style, F_content)
        minfn_args = {
            "args": (self.net, self.weights, self.layers, reprs, ratio),
            "method": grad_method, "jac": True, "bounds": data_bounds,
            "options": {"maxcor": 8, "maxiter": n_iter, "disp": verbose}
        }

        # 迭代優化
        self._callback = callback
        minfn_args["callback"] = self.callback
        if self.use_pbar and not verbose:
            self._create_pbar(n_iter)
            self.pbar.start()
            res = minimize(style_optfn, img0.flatten(), **minfn_args).nit
            self.pbar.finish()
        else:
            res = minimize(style_optfn, img0.flatten(), **minfn_args).nit

        return res

2.3 實現

2.3.1 pycaffe環境佈置

因為github上的程式碼是基於pycaffe的,所以需要配置環境

$ cd /home/shiyanlou/style-transfer
$ sudo gedit style.py

import caffe 之前加入python和pycaffe的環境變數

sys.path.append("/opt/caffe/python")
sys.path.append("/opt/caffe/python/caffe")

當然,待會轉換圖片時我們需要一個進度條的顯示,這裡我們使用 python progressbar

$ sudo pip install progressbar

2.3.2 下載模型

本實驗用到的是googlenet,如果你完成了上節課的作業,那麼你一定很清楚 googlenet 的結構了,其實和 VGG 一樣,我們只需幫他們當作一個輔助工具,這是大牛們用巨大的資料集(ImageNet)幫我們訓練好的可以準確提取圖片特徵的神經網路模型。下載完成之後需要將bvlc_googlenet.caffemodel放到指定路徑下 /style-transfer/models/googlenet

$ cd models/googlenet
$ wget http://labfile.oss.aliyuncs.com/courses/861/bvlc_googlenet.caffemodel

2.3.3 準備訓練

一個可以使用的訓練好的模型資料夾有三樣東西 style-transfer/models/googlenet

  • deploy.prototxt
  • ilsvrc_2012_mean.npy
  • bvlc_googlenet.caffemodel
  1. deploy.prototxt 網格配置檔案

    你可以通過如下指令來觀察網格的結構

    #和第二節一樣的操作
    $ cd /opt/caffe/python
    $ sudo apt-get install python-pydot
    $ sudo apt-get insall graphviz
    $ python draw_net.py /home/shiyanlou/style-transfer/models/googlenet/deploy.prototxt ~/Desktop/googlenet.png
    $ cd ~/Desktop
    $ display googlenet.png
    
  1. ilsvrc_2012_mean.npy 均值檔案(caffe中使用的均值資料格式是binaryproto,如果我們要使用python介面,或者我們要進行特徵視覺化,可能就要用到python格式的均值檔案了)。

    圖片減去均值後,再進行訓練和測試,會提高速度和精度。因此,一般在各種模型中都會有這個操作。那麼這個均值怎麼來的呢,實際上就是計算所有訓練樣本的平均值,計算出來後,儲存為一個均值檔案,在以後的測試中,就可以直接使用這個均值來相減,而不需要對測試圖片重新計算。

  2. bvlc_googlenet.caffemodel 訓練好的神經網路模型。

2.3.4 引數解析

我們來看看 /home/shiyanlou/style-transfer/style.py 中關於引數的設定,具體程式碼在88行。

$ python style.py -s <style_image> -c <content_image> -m <model_name> -g 0

主要引數解析

  • -s,風格圖位置
  • -c,內容圖位置
  • -m,模型位置
  • -g,什麼模式 -1為CPU,0為單個GPU,1為兩個GPU

其他預設或不必須引數

parser.add_argument("-r", "--ratio", default="1e4", type=str, required=False, help="style-to-content ratio")

非必要,轉化比率α/β,有預設值

parser.add_argument("-n", "--num-iters", default=512, type=int, required=False, help="L-BFGS iterations")

非必要,有預設值,表示迭代次數

2.3.5 開始轉換

$ cd /home/shiyanlou/style-transfer
$ python style.py -s images/style/starry_night.jpg -c images/content/nanjing.jpg -m googlenet -g -1 -n 20

我們使用的是梵高“星空”的風格,需要轉換風格的圖片是nanjing.jpg,使用googlenet模型及CPU,訓練20次的結果,如果操作正確的話,訓練的過程應該如下圖所示。

訓練

這裡的警告其實我們不用在意,這只是提醒我們轉換後的圖片與原始圖片的相對座標系發生了變換。

2.3.6 生成風格轉換圖片

我們可以在 /home/shiyanlou/style-transfer/images/style 中檢視風格圖;

/home/shiyanlou/style-transfer/images/content中檢視內容圖。

/home/shiyanlou/style-transfer/outputs/ 就可以看到我們訓練完成得到的效果圖,圖片的名字包含我們使用的模型、轉化比率、迭代次數。

效果圖

我們可以通過更改 style.py 來設定影象輸出的尺寸大小,例如你自己的照片影象大小是1024*500 ,更改輸出length=1024,可以獲得與原始影象一致的尺寸。不更改的話,程式中預設輸出是512寬度,和輸入原始影象一致的寬長比。

parser.add_argument("-l", "--length", default=1024, type=float, required=False, help="maximum image length")

def transfer_style(self, img_style, img_content, length=1024, ratio=1e4,
n_iter=512, init="-1", verbose=False, callback=None):

六、實驗總結

至此,本門課程的學習結束,我們完成了一個很有趣的深度學習的專案。通過利用大牛訓練的神經網路模型,還有我們自定義的loss函式,我們成功的讓電腦學會了大師的繪畫技巧,實現了以後是不是覺得其實並沒有自己想象的這麼難?

最近風格遷移的概念被炒的很火,Prisma也成了朋友圈的裝逼利器,但是當我們真正理解了演算法背後的原理,其實這也就是隻紙老虎。最後,拿著自己畫出來的圖片去朋友圈裝逼吧,記得遮蔽那些學過深度學習的人哦!

七、課後習題

  1. 請增加迭代次數和轉換比率,看看轉換風格後的圖片效果是否會有巨大的提升?
  2. 將風格圖片和內容圖片換成自己選擇的圖片,看看效果如何?

八、參考文獻