今天 reddit 上一篇帖子引起了熱議,博主 jamesonatfritz 稱他將原本具備 1.7M 參數(shù)的風(fēng)格遷移網(wǎng)絡(luò)減少到只有 11,868 個(gè)參數(shù),該網(wǎng)絡(luò)仍然能夠輸出風(fēng)格化的圖像。且量化后的最終網(wǎng)絡(luò)體積僅有 17 kB,非常適合移動(dòng) app。
jamesonatfritz 想解決神經(jīng)網(wǎng)絡(luò)的過(guò)參數(shù)化問(wèn)題,想要?jiǎng)?chuàng)建體積小但性能優(yōu)的神經(jīng)網(wǎng)絡(luò)。他所試驗(yàn)的第一個(gè)任務(wù)便是藝術(shù)風(fēng)格遷移。
GitHub 鏈接:https://github.com/fritzlabs/fritz-style-transfer
現(xiàn)在有很多用來(lái)訓(xùn)練藝術(shù)風(fēng)格遷移模型的現(xiàn)成工具,還有上千種開(kāi)源實(shí)現(xiàn)。其中的多數(shù)工具利用 Johnson 等人在《Perceptual Losses for Real-Time Style Transfer and Super-Resolution》中提出的網(wǎng)絡(luò)架構(gòu)的變體來(lái)實(shí)現(xiàn)快速、前饋的風(fēng)格化。因此,多數(shù)遷移模型的大小是 7MB。對(duì)于你的應(yīng)用來(lái)說(shuō),這個(gè)負(fù)擔(dān)并非不可承受,但也并非無(wú)足輕重。
研究表明,神經(jīng)網(wǎng)絡(luò)的體積通常遠(yuǎn)遠(yuǎn)大于所需,數(shù)百萬(wàn)的權(quán)重中有很多并不重要。因此作者創(chuàng)造了一個(gè)體積大大縮小的可靠風(fēng)格遷移模型:一個(gè)只有 11686 個(gè)訓(xùn)練權(quán)重的 17KB 神經(jīng)網(wǎng)絡(luò)。
左:原圖;中:來(lái)自上述 17KB 模型的風(fēng)格化圖像;右:來(lái)自 7MB 模型的風(fēng)格化圖像。
快速概覽:
原始模型:
大?。?MB
權(quán)重?cái)?shù):1.7M
在 iPhone X 上的速度:18 FPS
小模型:
大小:17KB
權(quán)重?cái)?shù):11,868
在 iPhone X 上的速度:29 FPS
如何縮小風(fēng)格遷移模型
作者主要使用了兩種技術(shù),而且都可以泛化到其他模型:
1. 大刀闊斧地修剪層和權(quán)重;
2. 通過(guò)量化將 32 位浮點(diǎn)權(quán)重轉(zhuǎn)換為 8 位整型
機(jī)器之心Synced剪枝小程序
修剪策略
卷積神經(jīng)網(wǎng)絡(luò)通常包含數(shù)百萬(wàn)甚至上億個(gè)需要在訓(xùn)練階段進(jìn)行調(diào)整的權(quán)重。通常來(lái)講,權(quán)重越多準(zhǔn)確率越高。但這種增加權(quán)重提高準(zhǔn)確率的做法非常低效。谷歌 MobileNetV2 的 stock 配置具有 347 萬(wàn)個(gè)權(quán)重,內(nèi)存占用達(dá) 16MB。InceptionV3 架構(gòu)大小約為前者的 6 倍,具備 2400 萬(wàn)個(gè)權(quán)重,內(nèi)存占用達(dá) 92MB。盡管多了 2000 多萬(wàn)個(gè)權(quán)重,但 InceptionV3 在 ImageNet 上的 top-1 分類準(zhǔn)確率只比 MobileNetV2 高出 7 個(gè)百分點(diǎn)(80% vs 73%)。
因此,我們可以假設(shè)神經(jīng)網(wǎng)絡(luò)中的多數(shù)權(quán)重沒(méi)有那么重要并將其移除。但重點(diǎn)是怎么做呢?我們可以選擇在三個(gè)層面進(jìn)行修剪:?jiǎn)蝹€(gè)權(quán)重、層、塊。
權(quán)重層面:假設(shè)某個(gè)神經(jīng)網(wǎng)絡(luò)上的多數(shù)(>95%)權(quán)重都沒(méi)有什么用。如果能找出那些對(duì)準(zhǔn)確率有影響的權(quán)重,就可以將其留下并將其他移除。
層層面:每個(gè)層中都包含一些權(quán)重。例如,2D 卷積層具有一個(gè)權(quán)重張量,即卷積核,用戶可以定義其寬度、高度和深度??s小卷積核可以減小整個(gè)網(wǎng)絡(luò)的大小。
塊層面:多個(gè)層通??梢越Y(jié)合成可重復(fù)利用的子圖,即塊。以 ResNet 為例,它的名字來(lái)源于重復(fù) 10-50 次的「殘差塊」。在塊層面進(jìn)行修剪可以移除多個(gè)層,從而一次性移除多個(gè)參數(shù)。
在實(shí)踐中,稀疏張量運(yùn)算沒(méi)有很好的實(shí)現(xiàn),因此權(quán)重層面的修剪沒(méi)有多大價(jià)值。那么就只剩下層和塊層面的修剪了。
實(shí)踐中的修剪
作者使用的層修剪技術(shù)是引入 width multiplier 作為超參數(shù)。width multiplier 最初由谷歌在其論文《MobileNets: Efficient Convolutional Neural Networks for Mobile Vision》中提出,非常簡(jiǎn)單、高效。
width multiplier 利用一個(gè)恒定系數(shù)調(diào)整每個(gè)卷積層中的卷積核數(shù)量。對(duì)于給定的層及 width multiplier alpha,卷積核數(shù)量 F 變?yōu)?alpha * F。
有了這個(gè)超參數(shù),我們就可以生成一系列架構(gòu)相同但權(quán)重?cái)?shù)不同的網(wǎng)絡(luò)。訓(xùn)練每種配置,就可以在模型速度、大小及準(zhǔn)確率之間做出權(quán)衡。
下面是作者模仿 Johnson 等人在《The Lottery Ticket Hypothesis: Finding Sparse, Trainable Neural Networks》提出的網(wǎng)絡(luò)架構(gòu)構(gòu)建快速風(fēng)格遷移模型的方法,不同之處在于添加了 width multiplier 作為超參數(shù)。
@classmethod
def build(
cls,
image_size,
alpha=1.0,
input_tensor=None,
checkpoint_file=None):
"""Build a Transfer Network Model using keras' functional API.
Args:
image_size - the size of the input and output image (H, W)
alpha - a width parameter to scale the number of channels by
Returns:
model: a keras model object
"""
x = keras.layers.Input(
shape=(image_size[0], image_size[1], 3), tensor=input_tensor)
out = cls._convolution(x, int(alpha * 32), 9, strides=1)
out = cls._convolution(out, int(alpha * 64), 3, strides=2)
out = cls._convolution(out, int(alpha * 128), 3, strides=2)
out = cls._residual_block(out, int(alpha * 128))
out = cls._residual_block(out, int(alpha * 128))
out = cls._residual_block(out, int(alpha * 128))
out = cls._residual_block(out, int(alpha * 128))
out = cls._residual_block(out, int(alpha * 128))
out = cls._upsample(out, int(alpha * 64), 3)
out = cls._upsample(out, int(alpha * 32), 3)
out = cls._convolution(out, 3, 9, relu=False, padding='same')
# Restrict outputs of pixel values to -1 and 1.
out = keras.layers.Activation('tanh')(out)
# Deprocess the image into valid image data. Note we'll need to define
# a custom layer for this in Core ML as well.
out = layers.DeprocessStylizedImage()(out)
model = keras.models.Model(inputs=x, outputs=out)
注意,模型構(gòu)建器類的其余部分沒(méi)有顯示。
當(dāng) alpha=1.0 時(shí),得到的網(wǎng)絡(luò)包含 170 萬(wàn)個(gè)權(quán)重。當(dāng) alpha=0.5 時(shí),得到的網(wǎng)絡(luò)僅有 424,102 個(gè)權(quán)重。
你可以構(gòu)建一些寬度參數(shù)很小的網(wǎng)絡(luò),但是也有相當(dāng)多的重復(fù)塊。作者決定修剪掉一些,但實(shí)際操作后卻發(fā)現(xiàn)不能移除太多。即使參數(shù)量保持不變,較深的網(wǎng)絡(luò)能夠產(chǎn)生更好的結(jié)果。作者最終刪除了五個(gè)殘差塊中的兩個(gè),并將每層的默認(rèn)濾波器數(shù)量減少至 32 個(gè)。得到的微型網(wǎng)絡(luò)如下所示:
@classmethod
def build(
cls,
image_size,
alpha=1.0,
input_tensor=None,
checkpoint_file=None):
"""Build a Small Transfer Network Model using keras' functional API.
This architecture removes some blocks of layers and reduces the size
of convolutions to save on computation.
Args:
image_size - the size of the input and output image (H, W)
alpha - a width parameter to scale the number of channels by
Returns:
model: a keras model object
"""
x = keras.layers.Input(
shape=(image_size[0], image_size[1], 3), tensor=input_tensor)
out = cls._convolution(x, int(alpha * 32), 9, strides=1)
out = cls._convolution(out, int(alpha * 32), 3, strides=2)
out = cls._convolution(out, int(alpha * 32), 3, strides=2)
out = cls._residual_block(out, int(alpha * 32))
out = cls._residual_block(out, int(alpha * 32))
out = cls._residual_block(out, int(alpha * 32))
out = cls._upsample(out, int(alpha * 32), 3)
out = cls._upsample(out, int(alpha * 32), 3)
out = cls._convolution(out, 3, 9, relu=False, padding='same')
# Restrict outputs of pixel values to -1 and 1.
out = keras.layers.Activation('tanh')(out)
# Deprocess the image into valid image data. Note we'll need to define
# a custom layer for this in Core ML as well.
out = layers.DeprocessStylizedImage()(out)
model = keras.models.Model(inputs=x, outputs=out)
帶有寬度參數(shù)的較小風(fēng)格遷移網(wǎng)絡(luò)。
通過(guò)反復(fù)嘗試,作者發(fā)現(xiàn)仍然可以用上述架構(gòu)實(shí)現(xiàn)良好的風(fēng)格遷移,一直到寬度參數(shù)為 0.3,在每一層上留下 9 個(gè)濾波器。最終結(jié)果是一個(gè)只有 11,868 個(gè)權(quán)重的神經(jīng)網(wǎng)絡(luò)。任何權(quán)重低于 10000 的網(wǎng)絡(luò)都不能持續(xù)訓(xùn)練,并且會(huì)產(chǎn)生糟糕的風(fēng)格化圖像。
值得一提的是,剪枝技術(shù)是在網(wǎng)絡(luò)訓(xùn)練之前應(yīng)用的。在訓(xùn)練期間或訓(xùn)練后反復(fù)修剪,你可以在很多任務(wù)上實(shí)現(xiàn)更高的性能。
量化
最后一段壓縮是在網(wǎng)絡(luò)訓(xùn)練完成后進(jìn)行的。神經(jīng)網(wǎng)絡(luò)權(quán)重通常存儲(chǔ)為 64 位或 32 位浮點(diǎn)數(shù)。量化過(guò)程將每一個(gè)浮點(diǎn)權(quán)重映射到具有較低位寬的整數(shù)。從 32 位浮點(diǎn)權(quán)重變?yōu)?8 位整型,使得存儲(chǔ)大小減少了 4 倍。作者利用 Alexis Creuzot 在博客中提出的方法(https://heartbeat.fritz.ai/reducing-coreml2-model-size-by-4x-with-quantization-in-ios12-b1c854651c4),在不怎么影響風(fēng)格的情況下使浮點(diǎn)數(shù)降低到了 8 位量化。
現(xiàn)在所有主要的移動(dòng)框架都支持量化,如 TensorFlow Mobile、TensorFlow Lite、Core ML 和 Caffe2Go。
最終結(jié)果
該微型網(wǎng)絡(luò)架構(gòu)有 11,868 個(gè)參數(shù),相比之下,Johnson 最初的模型具有 170 萬(wàn)個(gè)參數(shù),大小為 1.7MB。當(dāng)轉(zhuǎn)化為 Core ML 并量化時(shí),最終大小僅為 17KB——為原始大小的 1/400。以下是在梵高的《Starry Night》上的訓(xùn)練結(jié)果。
此微型風(fēng)格遷移結(jié)果的實(shí)時(shí)視頻可在 Heartbeat App 上查看:
http://bit.ly/heartbeat-ios
作者驚訝地發(fā)現(xiàn),盡管尺寸相差 400 倍,但在 iPhone X 上,這款微型模型的運(yùn)行速度僅快了 50%。原因可能是計(jì)算與這一通用架構(gòu)相關(guān),也可能是將圖像遷移到 GPU 進(jìn)行處理時(shí)造成的。
如果你對(duì)結(jié)果表示懷疑,可以自己下載并運(yùn)行此微型模型。甚至訓(xùn)練自己的模型!
下載地址:https://github.com/fritzlabs/fritz-style-transfer/blob/master/example/starry_night_640x480_small_a03_q8.mlmodel
總而言之,作者用兩種簡(jiǎn)單的技術(shù)將風(fēng)格遷移神經(jīng)網(wǎng)絡(luò)的規(guī)模減小了 99.75%。使用簡(jiǎn)單的 width multiplier 超參數(shù)修剪層,訓(xùn)練后的權(quán)重從 32 位浮點(diǎn)數(shù)量化為 8 位整數(shù)。未來(lái),作者期待看到將這些方法泛化到其它神經(jīng)網(wǎng)絡(luò)的效果。風(fēng)格遷移相對(duì)簡(jiǎn)單,因?yàn)椤笢?zhǔn)確率」肉眼可見(jiàn)。對(duì)于圖像識(shí)別這樣更加可以量化的任務(wù)而言,如此極端的修剪可能帶來(lái)更明顯的性能下降。
Reddit 討論
這篇帖子下有一些 reddit 網(wǎng)友對(duì)該項(xiàng)目提出了質(zhì)疑:
gwern:
看你的博客,剪枝部分似乎沒(méi)有移除任何層,只是更改了層的寬度/濾波器,然后對(duì)所有參數(shù)進(jìn)行量化。如果所有層都在(因?yàn)槟銢](méi)有做任何類似于訓(xùn)練較寬的淺層網(wǎng)絡(luò)的工作來(lái)模仿原始深度教師網(wǎng)絡(luò)),那么它們?nèi)詫暮罄m(xù)計(jì)算的每一層中引入大量延遲,即使每一層都很小。(由于你可以在手機(jī) GPU 上安裝更多模型,每個(gè)模型使用較少的 FLOPS,因此整體吞吐量會(huì)變得更好。但是每個(gè)模型的迭代仍然需要一段時(shí)間,在特定大小之后,每一層基本上是即時(shí)的。)
jamesonatfritz 回復(fù):
你說(shuō)得對(duì),濾波器剪枝部分確實(shí)沒(méi)有移除層,但是我去掉了兩個(gè)殘差塊,從而消除了一些層。整體 FLOPs 的降低情況不如全部權(quán)重?cái)?shù)量的減少情況,這一點(diǎn)你說(shuō)得對(duì)。不幸的是,Apple 沒(méi)法讓你較好地控制模型運(yùn)行的位置。你無(wú)法強(qiáng)制該模型使用 GPU。一些啟發(fā)式方法導(dǎo)致較小的模型僅在 CPU 上運(yùn)行,這是可能的。
gwern:
「去掉了兩個(gè)殘差塊,從而消除了一些層?!?/p>
你借此獲得了一些加速,但是使用更扁平的模型或許會(huì)實(shí)現(xiàn)更多加速。
jamesonatfritz 回復(fù):
確實(shí)如此。我試過(guò)的最扁平模型只有一個(gè)卷積層、一個(gè)殘差模塊和一個(gè)上采樣模塊,但我發(fā)現(xiàn)這些變體無(wú)法收斂。
gwern:
這似乎有些過(guò)了:只有一個(gè)層有些過(guò)于難了。我想的是三四個(gè)層這樣,在預(yù)訓(xùn)練風(fēng)格遷移模型的確切像素輸出上進(jìn)行訓(xùn)練。或許值得一試。
Ikuyas:
這個(gè)方法和直接使用小模型有什么區(qū)別嗎?我確定使用 11,868 個(gè)參數(shù)進(jìn)行訓(xùn)練結(jié)果會(huì)更好。另外,1.7M 參數(shù)太大了,每個(gè)參數(shù)的貢獻(xiàn)估計(jì)會(huì)很小。但是,真實(shí)情況是只有幾百個(gè)參數(shù)是真正重要的,其他參數(shù)只是到處吸收一點(diǎn)微小的噪聲。
從標(biāo)準(zhǔn)回歸的角度來(lái)看,這似乎是完美的預(yù)期結(jié)果。
作者回復(fù):
我應(yīng)該在文章里寫(xiě)清楚的,事實(shí)上你所說(shuō)的正是我所做的。剪枝發(fā)生在訓(xùn)練之前。反直覺(jué)的一件事是,實(shí)際上使用較少的參數(shù)從頭開(kāi)始訓(xùn)練模型無(wú)法確保能得到一樣的結(jié)果。盡管一小部分權(quán)重比較重要,但你很難提前知道哪些權(quán)重是重要的。詳情參見(jiàn)論文:《The Lottery Ticket Hypothesis: Finding Sparse, Trainable Neural Networks》。
Ikuyas:
神經(jīng)網(wǎng)絡(luò)中的參數(shù)缺乏有意義的解釋,這是第一堂機(jī)器學(xué)習(xí)課程中就學(xué)過(guò)的。這并不反直覺(jué),而是預(yù)料之中。剪枝后的參數(shù)甚至并不被認(rèn)為是吸收噪聲的神經(jīng)元。對(duì)于標(biāo)準(zhǔn)回歸模型來(lái)說(shuō),噪聲有時(shí)似乎像是正態(tài)分布的實(shí)現(xiàn)。而神經(jīng)網(wǎng)絡(luò)擬合并不假設(shè)任何此類事情。因此使用較少的參數(shù),你可以用完全不同的模型擬合數(shù)據(jù)。剪枝技術(shù)并沒(méi)有什么用。