數(shù)字營(yíng)銷是指在數(shù)字平臺(tái)上推廣服務(wù)和產(chǎn)品。廣告技術(shù)(通常簡(jiǎn)寫為“ad tech”)是指供應(yīng)商、品牌及其代理機(jī)構(gòu)使用數(shù)字技術(shù)來(lái)定位潛在客戶,提供個(gè)性化信息和產(chǎn)品,并分析線上花費(fèi)帶來(lái)的效果。例如,贊助的故事在Facebook新聞傳播里的傳播;在Instagram里的故事量;在YouTube的視頻內(nèi)容開始前播放的廣告;由Outbrain支持的美國(guó)有線電視新聞網(wǎng)文章末尾的建議鏈接,所有這些都是實(shí)際使用廣告技術(shù)的案例。
在過(guò)去的一年里,深度學(xué)習(xí)在數(shù)字營(yíng)銷和廣告技術(shù)中得到了顯著地應(yīng)用。
在這篇文章中,我們將深入探討一個(gè)流行的應(yīng)用場(chǎng)景的一部分:挖掘網(wǎng)絡(luò)名人認(rèn)可的商品。在此過(guò)程中,我們將能了解深度學(xué)習(xí)架構(gòu)的相對(duì)價(jià)值,進(jìn)行實(shí)驗(yàn),理解數(shù)據(jù)量大小的影響,以及在缺乏足夠數(shù)據(jù)時(shí)如何增強(qiáng)數(shù)據(jù)等內(nèi)容。
應(yīng)用場(chǎng)景概述
在本文中,我們將看到如何建立一個(gè)深度學(xué)習(xí)分類器,該分類器可以根據(jù)帶有商標(biāo)的圖片來(lái)預(yù)測(cè)該商品所對(duì)應(yīng)的公司。本節(jié)概述了可以使用此模型的場(chǎng)景。
名人會(huì)認(rèn)可一些產(chǎn)品。 通常,他們會(huì)在社交媒體上發(fā)布圖片來(lái)炫耀他們認(rèn)可的品牌。典型帖子會(huì)包含一張圖片,其中有名人自己和他們寫的一些文字。相對(duì)應(yīng)的,品牌的擁有者也渴望了解這些帖子的里他們品牌的展現(xiàn),并向可能受到影響的潛在客戶展示它們。
因此,這一廣告技術(shù)應(yīng)用的工作流程如下:將大量的帖子輸入處理程序以找出名人、品牌和文字內(nèi)容。然后,對(duì)于每個(gè)潛在客戶,機(jī)器學(xué)習(xí)模型會(huì)根據(jù)時(shí)間、地點(diǎn)、消息、品牌以及客戶的偏好品牌和其他內(nèi)容生成非常獨(dú)特的廣告。另外一個(gè)模型則進(jìn)行目標(biāo)客戶群的檢測(cè)。隨后進(jìn)行目標(biāo)廣告的發(fā)送。
圖1顯示了這一工作流程:

圖1 名人品牌認(rèn)可機(jī)器人的工作流程。圖片由Tuhin Sharma提供
如你所見,該系統(tǒng)由多個(gè)機(jī)器學(xué)習(xí)模型組成。
考慮一下上面所說(shuō)的圖像。這些照片可以是在任何情況下拍攝的。因此首要目標(biāo)就是確定照片中的物體和名人。這可以通過(guò)物體檢測(cè)模型完成。然后,下一步是識(shí)別品牌(如果有的話)。而識(shí)別品牌最簡(jiǎn)單的方法就是通過(guò)識(shí)別它的商標(biāo)。
在本文中,我們將研究如何構(gòu)建一個(gè)深度學(xué)習(xí)模型來(lái)通過(guò)圖像中的商標(biāo)識(shí)別品牌。 后續(xù)的文章將討論構(gòu)建機(jī)器人的其他部分(物體檢測(cè)、文本生成等)。
問題定義
本文中要解決的問題是:給定一張圖片,通過(guò)標(biāo)識(shí)圖片里的商標(biāo)來(lái)預(yù)測(cè)圖片對(duì)應(yīng)的公司(品牌)。
數(shù)據(jù)
要構(gòu)建機(jī)器學(xué)習(xí)模型,獲取高質(zhì)量數(shù)據(jù)集是必須的。在現(xiàn)實(shí)業(yè)務(wù)中,數(shù)據(jù)科學(xué)家會(huì)與品牌經(jīng)理和代理商合作來(lái)獲得所有可能的商標(biāo)。
為了本文的目的,我們將利用FlickrLogo數(shù)據(jù)集。該數(shù)據(jù)集有來(lái)自Flickr(一個(gè)流行的照片分享網(wǎng)站)的真實(shí)圖片。FlickrLogo頁(yè)面上有關(guān)于如何下載數(shù)據(jù)的說(shuō)明。如果你想使用本文中的代碼構(gòu)建自己的模型,請(qǐng)自行下載數(shù)據(jù)。
模型
從商標(biāo)標(biāo)識(shí)別品牌是一個(gè)經(jīng)典的計(jì)算機(jī)視覺問題。在過(guò)去的幾年中,深度學(xué)習(xí)已成為解決計(jì)算機(jī)視覺問題的最新技術(shù)。因此我們將為我們的場(chǎng)景構(gòu)建深度學(xué)習(xí)模型。
軟件
在之前的文章中,我們談到了Apache MXNet的優(yōu)點(diǎn)。我們還談到了Gluon這一基于MXNet的更簡(jiǎn)單的接口。兩者都非常強(qiáng)大,并允許深度學(xué)習(xí)工程師快速嘗試各種模型架構(gòu)。
現(xiàn)在讓我們來(lái)看看代碼。
用到的庫(kù)
我們首先導(dǎo)入構(gòu)建模型所需的庫(kù):
import mxnet as mx
import cv2
from pathlib import Path
import os
from time import time
import shutil
import matplotlib.pyplot as plt
%matplotlib inline
下載數(shù)據(jù)
我們使用FlickrLogos數(shù)據(jù)集里的FlickrLogos-32數(shù)據(jù)集。變量<flickrlogos-url>是這個(gè)數(shù)據(jù)集的URL。
%%capture
!wget -nc <flickrlogos-url> # Replace with the URL to the dataset
!unzip -n ./FlickrLogos-32_dataset_v2.zip
數(shù)據(jù)準(zhǔn)備
接著是創(chuàng)建下述的數(shù)據(jù)集:
1.Train (訓(xùn)練數(shù)據(jù)集)
2.Validation (驗(yàn)證數(shù)據(jù)集)
3.Test (測(cè)試數(shù)據(jù)集)
FlickrLogos數(shù)據(jù)已經(jīng)分好了訓(xùn)練、驗(yàn)證和測(cè)試數(shù)據(jù)集。下面是數(shù)據(jù)里圖片的信息。
- 訓(xùn)練數(shù)據(jù)集包括32個(gè)類別,每個(gè)類別有10張圖片。
- 驗(yàn)證數(shù)據(jù)集里有3960張圖片,其中3000張沒有包含商標(biāo)。
- 測(cè)試數(shù)據(jù)有3960張圖片。
所有的訓(xùn)練數(shù)據(jù)圖片都包含有商標(biāo),但有些驗(yàn)證和測(cè)試數(shù)據(jù)里的圖片沒有包含商標(biāo)。我們是希望構(gòu)建一個(gè)有比較好泛化能力的模型。即我們的模型可以準(zhǔn)確地預(yù)測(cè)它沒有見過(guò)的圖片(驗(yàn)證和測(cè)試的圖片)
為了讓我們的訓(xùn)練更快速、準(zhǔn)確,我們將把50%的沒有商標(biāo)的圖片從驗(yàn)證數(shù)據(jù)集移到訓(xùn)練數(shù)據(jù)集。這樣我們制作出大小為1820的訓(xùn)練數(shù)據(jù)集(在從驗(yàn)證數(shù)據(jù)集添加1500個(gè)無(wú)商標(biāo)圖像之后),并將驗(yàn)證數(shù)據(jù)集減少到2460張(在移出1500個(gè)無(wú)商標(biāo)圖像之后)。在現(xiàn)實(shí)生活中,我們應(yīng)該嘗試使用不同的模型架構(gòu)來(lái)選擇一個(gè)在實(shí)際驗(yàn)證和測(cè)試數(shù)據(jù)集上表現(xiàn)良好的模型架構(gòu)。
下一步,我們定義存儲(chǔ)數(shù)據(jù)的目錄。
data_directory = “./FlickrLogos-v2/”
現(xiàn)在定義訓(xùn)練、測(cè)試和驗(yàn)證數(shù)據(jù)列表的路徑。對(duì)于驗(yàn)證目錄,我們定義兩個(gè)路徑:一個(gè)存放包含商標(biāo)的圖片,另外一個(gè)用于沒有商標(biāo)的圖片。
train_logos_list_filename = data_directory+”trainset.relpaths.txt”
val_logos_list_filename = data_directory+”valset-logosonly.relpaths.txt”
val_nonlogos_list_filename = data_directory+”valset-nologos.relpaths.txt”
test_list_filename = data_directory+”testset.relpaths.txt”
讓我們從上面定義的列表里面讀入訓(xùn)練、測(cè)試和驗(yàn)證(帶有商標(biāo)和無(wú)商標(biāo)的)數(shù)據(jù)文件名。
從FlickrLogo數(shù)據(jù)集讀入的列表已經(jīng)被按照訓(xùn)練、測(cè)試和驗(yàn)證(包含和未包含商標(biāo))進(jìn)行了分類。
# List of train images
with open(train_logos_list_filename) as f:
train_logos_filename = f.read().splitlines()
# List of validation images without logos
with open(val_nonlogos_list_filename) as f:
val_nonlogos_filename = f.read().splitlines()
# List of validation images with logos
with open(val_logos_list_filename) as f:
val_logos_filename = f.read().splitlines()
# List of test images
with open(test_list_filename) as f:
test_filenames = f.read().splitlines()
現(xiàn)在讓我們把一些沒有商標(biāo)的驗(yàn)證圖片移動(dòng)到訓(xùn)練集里面去。這樣就讓訓(xùn)練數(shù)據(jù)里包含了原來(lái)所有的圖片,外加上來(lái)自驗(yàn)證數(shù)據(jù)里50%的沒有商標(biāo)的圖片。而驗(yàn)證數(shù)據(jù)集現(xiàn)在只包含原有的所有帶有商標(biāo)的圖片和剩下50%沒有商標(biāo)的圖片。
train_filenames = train_logos_filename + val_nonlogos_filename[0:int(len(val_nonlogos_filename)/2)]
val_filenames = val_logos_filename + val_nonlogos_filename[int(len(val_nonlogos_filename)/2):]
為了驗(yàn)證我們的數(shù)據(jù)準(zhǔn)備結(jié)果是對(duì)的,讓我們打印訓(xùn)練、測(cè)試和驗(yàn)證數(shù)據(jù)集里的圖片數(shù)量。
print(“Number of Training Images : “,len(train_filenames))
print(“Number of Validation Images : “,len(val_filenames))
print(“Number of Testing Images : “,len(test_filenames))
數(shù)據(jù)準(zhǔn)備過(guò)程的下一步是設(shè)置一種目錄結(jié)構(gòu)來(lái)讓模型的訓(xùn)練過(guò)程更容易一些。
我們需要目錄的結(jié)構(gòu)和圖2里的類似。

圖2 數(shù)據(jù)的目錄結(jié)構(gòu)。圖片由Tuhin Sharma提供
下面這個(gè)函數(shù)能幫助我們創(chuàng)建這個(gè)目錄結(jié)構(gòu)。
def prepare_datesets(base_directory,filenames,dest_folder_name):
for filename in filenames:
image_src_path = base_directory+filename
image_dest_path = image_src_path.replace(‘classes/jpg’,dest_folder_name)
dest_directory_path = Path(os.path.dirname(image_dest_path))
dest_directory_path.mkdir(parents=True,exist_ok=True)
shutil.copy2(image_src_path, image_dest_path)
使用這個(gè)函數(shù)來(lái)創(chuàng)建訓(xùn)練、驗(yàn)證和測(cè)試目錄,并把圖片按照它們的相應(yīng)的類別放到目錄里面。
prepare_datesets(base_directory=data_directory,filenames=train_filenames,dest_folder_name=’train_data’)
prepare_datesets(base_directory=data_directory,filenames=val_filenames,dest_folder_name=’val_data’)
prepare_datesets(base_directory=data_directory,filenames=test_filenames,dest_folder_name=’test_data’)
接下來(lái)是定義模型所用的超參數(shù)。
我們將會(huì)有33個(gè)類別(32種商標(biāo)和1個(gè)無(wú)商標(biāo))。這個(gè)數(shù)據(jù)量并不大,所以我們將只會(huì)使用一個(gè)GPU。我們將會(huì)訓(xùn)練20個(gè)周期,并使用40作為訓(xùn)練批次的大小。
batch_size = 40
num_classes = 33
num_epochs = 20
num_gpu = 1
ctx = [mx.gpu(i) for i in range(num_gpu)]
數(shù)據(jù)預(yù)處理
在圖片被導(dǎo)入后,我們需要確保圖片的尺寸是一致的。我們會(huì)把圖片重縮放成224*224像素大小。
我們有1820張訓(xùn)練圖片,但并不算很多。有沒有一個(gè)好的辦法來(lái)獲得更多的數(shù)據(jù)?確實(shí)是有的。一張圖片在被翻轉(zhuǎn)后依然是表示相同的事物,至少商標(biāo)還是一樣的。被隨機(jī)剪裁的商標(biāo)還依然是同一個(gè)商標(biāo)。
因此,我們沒有必要為訓(xùn)練來(lái)找更多的圖片。而是把現(xiàn)有的圖片通過(guò)翻轉(zhuǎn)和剪切進(jìn)行一定的變形來(lái)獲得更多的數(shù)據(jù)。這同時(shí)這還能幫助讓模型更加魯棒。
讓我們把50%的訓(xùn)練數(shù)據(jù)上下翻轉(zhuǎn),并把它們剪切成224*224像素大小。
train_augs = [
mx.image.HorizontalFlipAug(.5),
mx.image.RandomCropAug((224,224))
]
對(duì)于驗(yàn)證和測(cè)試數(shù)據(jù),讓我們按中心點(diǎn)剪切圖片成224*224像素大小?,F(xiàn)在所有的訓(xùn)練、測(cè)試和驗(yàn)證數(shù)據(jù)集都是224*224像素大了。
val_test_augs = [
mx.image.CenterCropAug((224,224))
]
為了實(shí)現(xiàn)對(duì)于圖片的轉(zhuǎn)換,我們定義了transform函數(shù)。這個(gè)函數(shù)按照輸入的數(shù)據(jù)和增強(qiáng)的類型,對(duì)數(shù)據(jù)進(jìn)行變換,以更新數(shù)據(jù)集。
def transform(data, label, augs):
data = data.astype(‘float32’)
for aug in augs:
data = aug(data)
# from (H x W x c) to (c x H x W)
data = mx.nd.transpose(data, (2,0,1))
return data, mx.nd.array([label]).asscalar().astype(‘float32’)
Gluon庫(kù)有一個(gè)工具函數(shù)可以從文件里導(dǎo)入圖片:mx.gluon.data.vision.ImageFolderDataset。這個(gè)函數(shù)需要數(shù)據(jù)按照?qǐng)D2所示的目錄結(jié)構(gòu)來(lái)存放。
這個(gè)函數(shù)接收如下的參數(shù):
- 數(shù)據(jù)存儲(chǔ)的根目錄路徑
- 一個(gè)是否需要把圖片轉(zhuǎn)換成灰度圖或是彩色圖(彩色是默認(rèn)選項(xiàng))的標(biāo)記
- 一個(gè)函數(shù)來(lái)接收數(shù)據(jù)(圖片)和它的標(biāo)簽,并將圖片轉(zhuǎn)換。
下面的代碼展示了在導(dǎo)入數(shù)據(jù)后如何對(duì)其進(jìn)行轉(zhuǎn)換:
train_imgs = mx.gluon.data.vision.ImageFolderDataset(
data_directory+’train_data’,
transform=lambda X, y: transform(X, y, train_augs))
相同的,對(duì)于驗(yàn)證和測(cè)試數(shù)據(jù)集,在導(dǎo)入后也會(huì)進(jìn)行相應(yīng)的轉(zhuǎn)換。
val_imgs = mx.gluon.data.vision.ImageFolderDataset(
data_directory+’val_data’,
transform=lambda X, y: transform(X, y, val_test_augs))
test_imgs = mx.gluon.data.vision.ImageFolderDataset(
data_directory+’test_data’,
transform=lambda X, y: transform(X, y, val_test_augs))
DataLoader是一個(gè)內(nèi)建的工具函數(shù)來(lái)從數(shù)據(jù)集里導(dǎo)入數(shù)據(jù),并返回迷你批次的數(shù)據(jù)。在上述步驟里,我們已經(jīng)定義了訓(xùn)練、驗(yàn)證和測(cè)試數(shù)據(jù)集(?train_imgs、val_imgs?、test_imgs相應(yīng)的)。
num_workers屬性讓我們可以指定為數(shù)據(jù)預(yù)處理所需的多進(jìn)程工作器的個(gè)數(shù)。
train_data = mx.gluon.data.DataLoader(train_imgs, batch_size,num_workers=1, shuffle=True)
val_data = mx.gluon.data.DataLoader(val_imgs, batch_size, num_workers=1)
test_data = mx.gluon.data.DataLoader(test_imgs, batch_size, num_workers=1)
現(xiàn)在數(shù)據(jù)已經(jīng)導(dǎo)入了,來(lái)讓我們看一看吧。讓我們寫一個(gè)叫show_images的工具函數(shù)來(lái)以網(wǎng)格形式顯示圖片。
def show_images(imgs, nrows, ncols, figsize=None):
“””plot a grid of images”””
figsize = (ncols, nrows)
_, figs = plt.subplots(nrows, ncols, figsize=figsize)
for i in range(nrows):
for j in range(ncols):
figs[i][j].imshow(imgs[i*ncols+j].asnumpy())
figs[i][j].axes.get_xaxis().set_visible(False)
figs[i][j].axes.get_yaxis().set_visible(False)
plt.show()
現(xiàn)在,用4行8列的形式來(lái)展示前32張圖片。
for X, _ in train_data:
# from (B x c x H x W) to (Bx H x W x c)
X = X.transpose((0,2,3,1)).clip(0,255)/255
show_images(X, 4, 8)
break

圖3 進(jìn)行變形后的圖片的網(wǎng)格化展示。圖片由Tuhin Sharma提供
上面代碼的運(yùn)行結(jié)果如圖3所示。一些圖片看起來(lái)是含有商標(biāo)的,不過(guò)也經(jīng)常被切掉了一部分。
用于訓(xùn)練的工具函數(shù)
本節(jié)內(nèi),我們會(huì)定義如下一些函數(shù):
- 在當(dāng)前處理的批次里獲取數(shù)據(jù)
- 評(píng)估模型的準(zhǔn)確度
- 訓(xùn)練模型
- 給定URL,獲取圖片數(shù)據(jù)
- 對(duì)給定的圖片,預(yù)測(cè)圖片的標(biāo)簽
第一個(gè)函數(shù)_get_batch會(huì)返回指定批次的數(shù)據(jù)和標(biāo)簽。
def _get_batch(batch, ctx):
“””return data and label on ctx”””
data, label = batch
return (mx.gluon.utils.split_and_load(data, ctx),
mx.gluon.utils.split_and_load(label, ctx),
data.shape[0])
函數(shù)evaluate_accuracy會(huì)返回模型的分類準(zhǔn)確度。針對(duì)本文的目的,我們這里選擇了一個(gè)簡(jiǎn)單的準(zhǔn)確度指標(biāo)。在實(shí)際項(xiàng)目里,準(zhǔn)確度指標(biāo)需要根據(jù)應(yīng)用的需求來(lái)設(shè)定。
def evaluate_accuracy(data_iterator, net, ctx):
acc = mx.nd.array([0])
n = 0.
for batch in data_iterator:
data, label, batch_size = _get_batch(batch, ctx)
for X, y in zip(data, label):
acc += mx.nd.sum(net(X).argmax(axis=1)==y).copyto(mx.cpu())
n += y.size
acc.wait_to_read()
return acc.asscalar() / n
下一個(gè)定義的函數(shù)是train函數(shù)。這是到目前為止我們要?jiǎng)?chuàng)建的最大的函數(shù)。
根據(jù)給出的模型、訓(xùn)練、測(cè)試和驗(yàn)證數(shù)據(jù)集,模型被按照指定的周期數(shù)訓(xùn)練。在之前的一篇文章里,我們對(duì)這個(gè)函數(shù)如何運(yùn)作進(jìn)行了更詳細(xì)的介紹。
一旦在驗(yàn)證數(shù)據(jù)集上獲得了最佳的準(zhǔn)確度,這個(gè)模型在此檢查點(diǎn)的結(jié)果會(huì)被存下來(lái)。在每個(gè)周期里,在訓(xùn)練、驗(yàn)證和測(cè)試數(shù)據(jù)集上的準(zhǔn)確度都會(huì)被打印出來(lái)。
def train(net, ctx, train_data, val_data, test_data, batch_size, num_epochs, model_prefix, hybridize=False, learning_rate=0.01, wd=0.001):
net.collect_params().reset_ctx(ctx)
if hybridize == True:
net.hybridize()
loss = mx.gluon.loss.SoftmaxCrossEntropyLoss()
trainer = mx.gluon.Trainer(net.collect_params(), ‘sgd’, {
‘learning_rate’: learning_rate, ‘wd’: wd})
best_epoch = -1
best_acc = 0.0
if isinstance(ctx, mx.Context):
ctx = [ctx]
for epoch in range(num_epochs):
train_loss, train_acc, n = 0.0, 0.0, 0.0
start = time()
for i, batch in enumerate(train_data):
data, label, batch_size = _get_batch(batch, ctx)
losses = []
with mx.autograd.record():
outputs = [net(X) for X in data]
losses = [loss(yhat, y) for yhat, y in zip(outputs, label)]
for l in losses:
l.backward()
train_loss += sum([l.sum().asscalar() for l in losses])
trainer.step(batch_size)
n += batch_size
train_acc = evaluate_accuracy(train_data, net, ctx)
val_acc = evaluate_accuracy(val_data, net, ctx)
test_acc = evaluate_accuracy(test_data, net, ctx)
print(“Epoch %d. Loss: %.3f, Train acc %.2f, Val acc %.2f, Test acc %.2f, Time %.1f sec” % (
epoch, train_loss/n, train_acc, val_acc, test_acc, time() – start
))
if val_acc > best_acc:
best_acc = val_acc
if best_epoch!=-1:
print(‘Deleting previous checkpoint…’)
os.remove(model_prefix+’-%d.params’%(best_epoch))
best_epoch = epoch
print(‘Best validation accuracy found. Checkpointing…’)
net.collect_params().save(model_prefix+’-%d.params’%(epoch))
函數(shù)get_image會(huì)根據(jù)給定的URL返回一個(gè)圖片。這個(gè)函數(shù)可以用來(lái)測(cè)試模型的準(zhǔn)確度。
def get_image(url, show=False):
# download and show the image
fname = mx.test_utils.download(url)
img = cv2.cvtColor(cv2.imread(fname), cv2.COLOR_BGR2RGB)
img = cv2.resize(img, (224, 224))
plt.imshow(img)
return fname
最后一個(gè)工具函數(shù)是classify_logo。給定圖片和模型,這個(gè)函數(shù)將會(huì)返回此圖片的分類(在我們的場(chǎng)景里就是品牌的名字)和此分類相應(yīng)的概率。
def classify_logo(net, url):
fname = get_image(url)
with open(fname, ‘rb’) as f:
img = mx.image.imdecode(f.read())
data, _ = transform(img, -1, val_test_augs)
data = data.expand_dims(axis=0)
out = net(data.as_in_context(ctx[0]))
out = mx.nd.SoftmaxActivation(out)
pred = int(mx.nd.argmax(out, axis=1).asscalar())
prob = out[0][pred].asscalar()
label = train_imgs.synsets
return ‘With prob=%f, %s’%(prob, label[pred])
模型
理解模型的架構(gòu)是非常重要的。在我們之前的那篇文章里,我們構(gòu)建了一個(gè)多層感知機(jī)(MLP)。此架構(gòu)如圖4所示。

圖4 多層感知機(jī)。圖片由Tuhin Sharma提供
此MLP模型的輸入層應(yīng)該是怎么樣的?我的數(shù)據(jù)圖片的尺寸是224 * 224像素。
構(gòu)建輸入層的最常見的方法就是把圖片打平,構(gòu)建一個(gè)50176個(gè)(224 * 224)神經(jīng)元的輸入層。這就形成了一個(gè)如圖5所示的簡(jiǎn)單的數(shù)據(jù)流。

圖5 扁平化輸入。圖片由Tuhin Sharma提供
但是當(dāng)進(jìn)行這樣的扁平化處理后,圖像數(shù)據(jù)里的很多空間信息被丟失了。同時(shí)另外一個(gè)挑戰(zhàn)是相應(yīng)的權(quán)重?cái)?shù)量。如果第一個(gè)隱藏層有30個(gè)神經(jīng)元,那么這個(gè)模型的參數(shù)將會(huì)有50176 * 30再加上30個(gè)偏置量。因此,這看來(lái)不像是一個(gè)好的為圖像建模的方法。
現(xiàn)在讓我們來(lái)討論一下一個(gè)更合適的架構(gòu):用于圖片分類的卷積神經(jīng)網(wǎng)絡(luò)(CNN)。
卷積神經(jīng)網(wǎng)絡(luò)(CNN)
CNN和MLP類似,因?yàn)樗彩菢?gòu)建神經(jīng)網(wǎng)絡(luò)并為神經(jīng)元學(xué)習(xí)權(quán)重。CNN和MLP的關(guān)鍵的區(qū)別是輸入數(shù)據(jù)是圖片。CNN允許我們?cè)诩軜?gòu)里充分利用圖片的特性。
CNN有一些卷積層。這個(gè)詞匯“卷積”是來(lái)自圖像處理領(lǐng)域,如圖6所述。它工作于一個(gè)較小的窗口,叫做“感知域”,而不是處理來(lái)自前一層的所有輸入。這種機(jī)制就可以讓模型學(xué)習(xí)局部的特征。
每個(gè)卷積層用一個(gè)小矩陣(叫卷積核)在進(jìn)入本層的圖像上面的一部分上移動(dòng)。卷積會(huì)對(duì)卷積矩陣內(nèi)的每個(gè)像素進(jìn)行修改,此運(yùn)算可以幫助識(shí)別邊緣。圖6的左邊展示了一個(gè)圖片,中間是一個(gè)3×3的卷積核,而運(yùn)用此卷積核對(duì)左邊圖片的左上角像素計(jì)算的結(jié)果顯示在右邊圖里。我們還能定義多個(gè)卷積核,來(lái)表示不同的特征圖。

圖6 卷積層。圖片由Tuhin Sharma提供
在圖6的例子里,輸入的圖片的尺寸是5×5,而卷積核的尺寸是3×3。卷積計(jì)算是兩個(gè)矩陣的元素與元素的乘積之和。例子里卷積的輸出尺寸也是5×5。
為了理解這些,我們需要理解卷積層里的兩個(gè)重要參數(shù):步長(zhǎng)(stride)和填充方法(padding)。
步長(zhǎng)控制卷積核(過(guò)濾器)如何在圖片上移動(dòng)。
圖7表明了卷積核從第一個(gè)像素到第二個(gè)像素的移動(dòng)過(guò)程。

圖7 卷積核的移動(dòng)。圖片由Tuhin Sharma提供
在圖7里,步長(zhǎng)是1。
當(dāng)對(duì)一個(gè)5×5的圖片進(jìn)行3×3的卷積計(jì)算后,我們將得到一個(gè)3×3的圖片。針對(duì)這一情況,我們會(huì)在圖片的邊緣進(jìn)行填充?,F(xiàn)在這個(gè)5×5的圖片被0所圍繞,如圖8所示。

圖8 用0填充邊緣。圖片由Tuhin Sharma提供
這樣,當(dāng)我們用3×3卷積核計(jì)算時(shí),將會(huì)獲得一個(gè)5×5的輸出。
因此對(duì)于圖6所示的計(jì)算,它的步長(zhǎng)是1,且填充的尺寸也是1。
CNN比相應(yīng)的MLP能極大地減少權(quán)重的數(shù)量。假設(shè)我們使用30個(gè)卷積核,每個(gè)是3×3。每個(gè)卷積核的參數(shù)是3×3=9,外加1個(gè)偏置量。這樣每個(gè)卷積核有10個(gè)權(quán)重,總共30個(gè)卷積核就是300個(gè)權(quán)重。而在前面的章節(jié)里,MLP則是有150000個(gè)權(quán)重。
下一層一般典型地是一個(gè)子抽樣層。一旦我們識(shí)別了特征,這一子抽樣層會(huì)簡(jiǎn)化這個(gè)信息。一個(gè)常用的方法是最大池化。它從卷積層輸出的局部區(qū)域輸出最大值(見圖9)。這一層在保留了每個(gè)局部區(qū)域的最大激活特征的同時(shí),降低了輸出的尺寸。

圖9 最大池化。圖片由Tuhin Sharma提供
可以看到最大池化在保留了每個(gè)局部區(qū)域的最大激活特征的同時(shí),降低了輸出的尺寸。
想了解關(guān)于CNN的更多的信息,一個(gè)好的資源是這本在線圖書《神經(jīng)網(wǎng)絡(luò)和深度學(xué)習(xí)》。另外一個(gè)好的資源是斯坦福大學(xué)的CNN課程。
現(xiàn)在我們已經(jīng)對(duì)什么是CNN有了基本的了解。為了這里的問題,讓我們用gluon來(lái)實(shí)現(xiàn)它。
第一步是定義這個(gè)架構(gòu):
cnn_net = mx.gluon.nn.Sequential()
with cnn_net.name_scope():
#? First convolutional layer
cnn_net.add(mx.gluon.nn.Conv2D(channels=96, kernel_size=11, strides=(4,4), activation=’relu’))
cnn_net.add(mx.gluon.nn.MaxPool2D(pool_size=3, strides=2))
#? Second convolutional layer
cnn_net.add(mx.gluon.nn.Conv2D(channels=192, kernel_size=5, activation=’relu’))
cnn_net.add(mx.gluon.nn.MaxPool2D(pool_size=3, strides=(2,2)))
# Flatten and apply fullly connected layers
cnn_net.add(mx.gluon.nn.Flatten())
cnn_net.add(mx.gluon.nn.Dense(4096, activation=”relu”))
cnn_net.add(mx.gluon.nn.Dense(num_classes))
在模型架構(gòu)被定義好之后,讓我們初始化網(wǎng)絡(luò)里的權(quán)重。我們將使用Xavier初始器。
cnn_net.collect_params().initialize(mx.init.Xavier(magnitude=2.24), ctx=ctx)
權(quán)重初始化完后,我們可以訓(xùn)練模型了。我們會(huì)調(diào)用之前定義的相同的train函數(shù),并傳給它所需的參數(shù)。
train(cnn_net, ctx, train_data, val_data, test_data, batch_size, num_epochs,model_prefix=’cnn’)
Epoch 0. Loss: 53.771, Train acc 0.77, Val acc 0.58, Test acc 0.72, Time 224.9 sec
Best validation accuracy found. Checkpointing…
Epoch 1. Loss: 3.417, Train acc 0.80, Val acc 0.60, Test acc 0.73, Time 222.7 sec
Deleting previous checkpoint…
Best validation accuracy found. Checkpointing…
Epoch 2. Loss: 3.333, Train acc 0.81, Val acc 0.60, Test acc 0.74, Time 222.5 sec
Deleting previous checkpoint…
Best validation accuracy found. Checkpointing…
Epoch 3. Loss: 3.227, Train acc 0.82, Val acc 0.61, Test acc 0.75, Time 222.4 sec
Deleting previous checkpoint…
Best validation accuracy found. Checkpointing…
Epoch 4. Loss: 3.079, Train acc 0.82, Val acc 0.61, Test acc 0.75, Time 222.0 sec
Deleting previous checkpoint…
Best validation accuracy found. Checkpointing…
Epoch 5. Loss: 2.850, Train acc 0.82, Val acc 0.61, Test acc 0.76, Time 222.7 sec
Deleting previous checkpoint…
Best validation accuracy found. Checkpointing…
Epoch 6. Loss: 2.488, Train acc 0.82, Val acc 0.61, Test acc 0.76, Time 222.1 sec
Epoch 7. Loss: 1.943, Train acc 0.82, Val acc 0.61, Test acc 0.76, Time 221.3 sec
Epoch 8. Loss: 1.395, Train acc 0.82, Val acc 0.61, Test acc 0.76, Time 223.6 sec
Epoch 9. Loss: 1.146, Train acc 0.82, Val acc 0.61, Test acc 0.76, Time 222.5 sec
Epoch 10. Loss: 1.089, Train acc 0.82, Val acc 0.61, Test acc 0.76, Time 221.5 sec
Epoch 11. Loss: 1.078, Train acc 0.82, Val acc 0.61, Test acc 0.76, Time 220.7 sec
Epoch 12. Loss: 1.078, Train acc 0.82, Val acc 0.61, Test acc 0.76, Time 221.1 sec
Epoch 13. Loss: 1.075, Train acc 0.82, Val acc 0.61, Test acc 0.76, Time 221.3 sec
Epoch 14. Loss: 1.076, Train acc 0.82, Val acc 0.61, Test acc 0.76, Time 221.3 sec
Epoch 15. Loss: 1.076, Train acc 0.82, Val acc 0.61, Test acc 0.76, Time 220.4 sec
Epoch 16. Loss: 1.075, Train acc 0.82, Val acc 0.61, Test acc 0.76, Time 221.3 sec
Epoch 17. Loss: 1.074, Train acc 0.82, Val acc 0.61, Test acc 0.76, Time 221.8 sec
Epoch 18. Loss: 1.074, Train acc 0.82, Val acc 0.61, Test acc 0.76, Time 221.8 sec
Epoch 19. Loss: 1.073, Train acc 0.82, Val acc 0.61, Test acc 0.76, Time 220.9 sec
我們讓模型運(yùn)行20個(gè)周期。典型的情況是,我們會(huì)訓(xùn)練非常多的周期,并選擇驗(yàn)證準(zhǔn)確度最高的那個(gè)模型。在上面運(yùn)行了20個(gè)周期后,我們可以在日志里看到驗(yàn)證準(zhǔn)確度最高的是在周期5。在此周期之后,模型看起來(lái)并沒有學(xué)到更多。有可能網(wǎng)絡(luò)已經(jīng)飽和,學(xué)習(xí)速度變慢了。我們下一節(jié)里會(huì)試一個(gè)更好的方法,但還是先讓我們看看現(xiàn)在這個(gè)模型的表現(xiàn)如何。
讓我們把最佳驗(yàn)證準(zhǔn)確度的模型參數(shù)導(dǎo)入,然后分配給我們的模型:
cnn_net.collect_params().load(‘cnn-%d.params’%(5),ctx)
現(xiàn)在讓我們看看這個(gè)模型在新數(shù)據(jù)上的表現(xiàn)。我們會(huì)從網(wǎng)上獲取一個(gè)容易識(shí)別的圖片(見圖10),并看看模型是否能準(zhǔn)確地識(shí)別。
img_url = “http://sophieswift.com/wp-content/uploads/2017/09/pleasing-ideas-bmw-cake-and-satisfying-some-bmw-themed-cakes-crustncakes-delicious-cakes-128×128.jpg”
classify_logo(cnn_net, img_url)
‘With prob=0.081522, no-logo’

圖10 BMW的商標(biāo)。圖片由Tuhin Sharma提供
模型的預(yù)測(cè)結(jié)果很糟糕。它認(rèn)為這個(gè)圖片里沒有包含商標(biāo)的概率是8%。預(yù)測(cè)的結(jié)果是錯(cuò)的,且概率非常低。
讓我們?cè)僭囈粡垐D片(見圖11)來(lái)看看準(zhǔn)確率是否有改善。
img_url = “https://dtgxwmigmg3gc.cloudfront.net/files/59cdcd6f52ba0b36b5024500-icon-256×256.png”
classify_logo(cnn_net, img_url)
‘With prob=0.075301, no-logo’

圖11 Foster的商標(biāo)。圖片由Tuhin Sharma提供
又一次,模型的預(yù)測(cè)結(jié)果是錯(cuò)的,且概率很低。
我們沒有太多的數(shù)據(jù),而如上所見,模型訓(xùn)練得已經(jīng)飽和。我們可以繼續(xù)試驗(yàn)更多的模型架構(gòu),但是我們沒法克服小數(shù)據(jù)集的問題,因?yàn)榭捎?xùn)練的參數(shù)遠(yuǎn)遠(yuǎn)大于訓(xùn)練圖片的數(shù)量。那我們?nèi)绾慰朔@個(gè)問題?在沒有太多數(shù)據(jù)的情況下不能使用深度學(xué)習(xí)嗎?
對(duì)此的答案就是遷移學(xué)習(xí)。下面接著討論。
遷移學(xué)習(xí)
想想這個(gè)比喻。你想學(xué)一門新的外語(yǔ),怎么進(jìn)行哪?
例如,你可以進(jìn)行一個(gè)對(duì)話。導(dǎo)師:你好嗎?你:我很好,你怎么樣?
你也能試著對(duì)新的外語(yǔ)做一樣的事。
因?yàn)槟愕挠⒄Z(yǔ)很熟練,你可以不用從零開始學(xué)一門新的語(yǔ)言(即使看起來(lái)你是這樣做的)。你對(duì)一門語(yǔ)言已經(jīng)有了心智圖,你可以試著在新的語(yǔ)言里找到相應(yīng)的詞匯。因此在新的語(yǔ)言里,你的詞匯表可能依然有限,但憑借你對(duì)于英語(yǔ)對(duì)話結(jié)構(gòu)的知識(shí),你依然能用新語(yǔ)言進(jìn)行對(duì)話。
遷移學(xué)習(xí)的工作機(jī)制也是一樣的。高準(zhǔn)確度模型是在海量數(shù)據(jù)集上訓(xùn)練出來(lái)的。一個(gè)常見的數(shù)據(jù)集是ImageNet數(shù)據(jù)集。它有超過(guò)一百萬(wàn)張圖片。全球的研究人員已經(jīng)使用這個(gè)數(shù)據(jù)構(gòu)建了很多不同的前沿的模型。這些模型(包括模型的架構(gòu)和訓(xùn)練好的權(quán)重)都在網(wǎng)絡(luò)上可以獲得。
通過(guò)這些預(yù)先訓(xùn)練好的模型,我們將再次對(duì)于我們的問題來(lái)訓(xùn)練這個(gè)模型。事實(shí)上,這種情況是非常普通的。幾乎總是這樣的,大家首先構(gòu)建的能解決計(jì)算機(jī)視覺問題的模型都是使用了一個(gè)預(yù)先訓(xùn)練好的模型。
在諸如我們的例子的很多場(chǎng)景里,如果受限于數(shù)據(jù),這可能是所有人都能做的。
一個(gè)典型的方法是保持模型的前面層不變,只訓(xùn)練最后一層。如果數(shù)據(jù)量非常有限,只需要把分類層再訓(xùn)練就可以了。如果數(shù)據(jù)量相對(duì)充足,可以把最后的幾層都再訓(xùn)練一下。
這是有效的,因?yàn)榫矸e神經(jīng)網(wǎng)絡(luò)在每個(gè)連續(xù)層學(xué)習(xí)更高層次的表示。它在許多早期階段所做的學(xué)習(xí)在所有圖像分類問題中都是共同的。
現(xiàn)在讓我們使用一個(gè)預(yù)先訓(xùn)練的模型來(lái)做商標(biāo)檢測(cè)。
MXNet有一個(gè)模型庫(kù),里面有很多預(yù)先訓(xùn)練好的模型。
我們會(huì)使用一個(gè)流行的預(yù)先訓(xùn)練的模型,它叫resnet。這篇論文里提供了這個(gè)模型架構(gòu)的很多細(xì)節(jié)內(nèi)容。一個(gè)簡(jiǎn)單一些的解釋可以在這篇文章里找到。
讓我們先下載這個(gè)預(yù)訓(xùn)練過(guò)的模型:
from mxnet.gluon.model_zoo import vision as models
pretrained_net = models.resnet18_v2(pretrained=True)
因?yàn)槲覀兊臄?shù)據(jù)很少,所以我們將只訓(xùn)練最后的輸出層。我們先隨機(jī)初始化輸出層的權(quán)重。
finetune_net = models.resnet18_v2(classes=num_classes)
finetune_net.features = pretrained_net.features
finetune_net.output.initialize(mx.init.Xavier(magnitude=2.24))
現(xiàn)在我們調(diào)用之前一樣的訓(xùn)練函數(shù):
train(finetune_net, ctx, train_data, val_data, test_data, batch_size, num_epochs,model_prefix=’ft’,hybridize = True)
Epoch 0. Loss: 1.107, Train acc 0.83, Val acc 0.62, Test acc 0.76, Time 246.1 sec
Best validation accuracy found. Checkpointing…
Epoch 1. Loss: 0.811, Train acc 0.85, Val acc 0.62, Test acc 0.77, Time 243.7 sec
Deleting previous checkpoint…
Best validation accuracy found. Checkpointing…
Epoch 2. Loss: 0.722, Train acc 0.86, Val acc 0.64, Test acc 0.78, Time 245.3 sec
Deleting previous checkpoint…
Best validation accuracy found. Checkpointing…
Epoch 3. Loss: 0.660, Train acc 0.87, Val acc 0.66, Test acc 0.79, Time 243.4 sec
Deleting previous checkpoint…
Best validation accuracy found. Checkpointing…
Epoch 4. Loss: 0.541, Train acc 0.88, Val acc 0.67, Test acc 0.80, Time 244.5 sec
Deleting previous checkpoint…
Best validation accuracy found. Checkpointing…
Epoch 5. Loss: 0.528, Train acc 0.89, Val acc 0.68, Test acc 0.80, Time 243.4 sec
Deleting previous checkpoint…
Best validation accuracy found. Checkpointing…
Epoch 6. Loss: 0.490, Train acc 0.90, Val acc 0.68, Test acc 0.81, Time 243.2 sec
Deleting previous checkpoint…
Best validation accuracy found. Checkpointing…
Epoch 7. Loss: 0.453, Train acc 0.91, Val acc 0.71, Test acc 0.82, Time 243.6 sec
Deleting previous checkpoint…
Best validation accuracy found. Checkpointing…
Epoch 8. Loss: 0.435, Train acc 0.92, Val acc 0.70, Test acc 0.82, Time 245.6 sec
Epoch 9. Loss: 0.413, Train acc 0.92, Val acc 0.72, Test acc 0.82, Time 247.7 sec
Deleting previous checkpoint…
Best validation accuracy found. Checkpointing…
Epoch 10. Loss: 0.392, Train acc 0.92, Val acc 0.72, Test acc 0.83, Time 245.3 sec
Deleting previous checkpoint…
Best validation accuracy found. Checkpointing…
Epoch 11. Loss: 0.377, Train acc 0.92, Val acc 0.72, Test acc 0.83, Time 244.5 sec
Epoch 12. Loss: 0.335, Train acc 0.93, Val acc 0.72, Test acc 0.84, Time 244.2 sec
Epoch 13. Loss: 0.321, Train acc 0.94, Val acc 0.73, Test acc 0.84, Time 245.0 sec
Deleting previous checkpoint…
Best validation accuracy found. Checkpointing…
Epoch 14. Loss: 0.305, Train acc 0.93, Val acc 0.73, Test acc 0.84, Time 243.4 sec
Deleting previous checkpoint…
Best validation accuracy found. Checkpointing…
Epoch 15. Loss: 0.298, Train acc 0.93, Val acc 0.73, Test acc 0.84, Time 243.9 sec
Epoch 16. Loss: 0.296, Train acc 0.94, Val acc 0.75, Test acc 0.84, Time 247.0 sec
Deleting previous checkpoint…
Best validation accuracy found. Checkpointing…
Epoch 17. Loss: 0.274, Train acc 0.94, Val acc 0.74, Test acc 0.84, Time 245.1 sec
Epoch 18. Loss: 0.292, Train acc 0.94, Val acc 0.74, Test acc 0.84, Time 243.9 sec
Epoch 19. Loss: 0.306, Train acc 0.95, Val acc 0.73, Test acc 0.84, Time 244.8 sec
現(xiàn)在這個(gè)模型馬上就有了更好的準(zhǔn)確度。典型的情況是,當(dāng)數(shù)據(jù)比較少時(shí),我們只訓(xùn)練幾個(gè)周期,再選驗(yàn)證準(zhǔn)確度最高的那個(gè)周期的模型。
現(xiàn)在,周期16有最好的驗(yàn)證準(zhǔn)確度。因?yàn)橛?xùn)練數(shù)據(jù)有限,再繼續(xù)訓(xùn)練的話,模型就開始出現(xiàn)過(guò)擬合。我們可以看到在周期16后,隨著訓(xùn)練準(zhǔn)確度的增加,驗(yàn)證準(zhǔn)確度卻開始下降了。
讓我們導(dǎo)入周期16相應(yīng)的檢查點(diǎn)存儲(chǔ)的參數(shù),并用它們作為最后的模型。
# The model’s parameters are now set to the values at the 16th epoch
finetune_net.collect_params().load(‘ft-%d.params’%(16),ctx)
評(píng)估預(yù)測(cè)的結(jié)果
對(duì)于我們之前用于評(píng)估的相同的圖片,讓我們看看新模型的預(yù)測(cè)結(jié)果。
img_url = “http://sophieswift.com/wp-content/uploads/2017/09/pleasing-ideas-bmw-cake-and-satisfying-some-bmw-themed-cakes-crustncakes-delicious-cakes-128×128.jpg”
classify_logo(finetune_net, img_url)
‘With prob=0.983476, bmw’

圖12圖片由Tuhin Sharma提供
我們看到這個(gè)模型用98%的概率預(yù)測(cè)對(duì)了BMW這個(gè)商標(biāo)。
現(xiàn)在讓我們?cè)囈辉嚵硗庖粋€(gè)之前測(cè)試的圖片。
img_url = “https://dtgxwmigmg3gc.cloudfront.net/files/59cdcd6f52ba0b36b5024500-icon-256×256.png”
classify_logo(finetune_net, img_url)
‘With prob=0.498218, fosters’
盡管預(yù)測(cè)的概率并不高,比50%略低。但Foster依然是所有商標(biāo)里概率最高的那個(gè)。
進(jìn)一步改進(jìn)這個(gè)模型
為了改進(jìn)模型,我們需要改正一下我們構(gòu)建訓(xùn)練數(shù)據(jù)集的方式。每個(gè)商標(biāo)都有10張訓(xùn)練圖。但是,為了把一些驗(yàn)證圖像從驗(yàn)證集里轉(zhuǎn)到訓(xùn)練集,我們將1500張圖像作為無(wú)商標(biāo)的例子移動(dòng)到訓(xùn)練集里。這導(dǎo)致了顯著的數(shù)據(jù)偏差,這可不是一個(gè)好方法。以下是解決此問題的一些選項(xiàng):
- 給交叉熵?fù)p失賦予一定的權(quán)重。
- 在訓(xùn)練數(shù)據(jù)里不包含無(wú)商標(biāo)的圖片。從而訓(xùn)練一個(gè)對(duì)不包含商標(biāo)的測(cè)試和驗(yàn)證圖片預(yù)測(cè)成所有商標(biāo)都有很低的概率的模型。
但請(qǐng)記住,即使在用遷移學(xué)習(xí)和數(shù)據(jù)增強(qiáng)的情況下,我們也只有320個(gè)商標(biāo)圖片。這對(duì)于想建立高度精確的深度學(xué)習(xí)模型而言還是太少了。
總結(jié)
在本文中,我們學(xué)習(xí)了如何使用MXNet構(gòu)建圖像識(shí)別模型。Gluon是進(jìn)行快速原型試驗(yàn)的理想選擇。使用雜交和符號(hào)導(dǎo)出,把原型轉(zhuǎn)變成生產(chǎn)系統(tǒng)也非常容易。 通過(guò)使用MXNet上大量預(yù)先訓(xùn)練的模型,我們能夠在相當(dāng)快的時(shí)間內(nèi)獲得非常好的商標(biāo)檢測(cè)模型。一個(gè)很好的學(xué)習(xí)這背后的理論的資源是斯坦福大學(xué)的CS231n課程。
這篇博文是O’Reilly和Amazon的合作產(chǎn)物。請(qǐng)閱讀我們的編輯獨(dú)立聲明。

Tuhin Sharma住在印度班加羅爾,目前在Ivanti擔(dān)任高級(jí)數(shù)據(jù)科學(xué)家。 在此之前,他曾在IBM Watson和RedHat擔(dān)任數(shù)據(jù)科學(xué)家。他研究生畢業(yè)于印度理工學(xué)院計(jì)算機(jī)科學(xué)與工程專業(yè)。 他喜歡在休閑時(shí)間打乒乓球和吉他。他最喜歡的一句話是“生活是美好的”。 你可以在LinkedIn上找到他。


