大数据文摘授权转载自OReillyData
数字营销是指在数字平台上推广服务和产品。广告技术(通常简写为“adtech”)是指供应商、品牌及其代理机构使用数字技术来定位潜在客户,提供个性化信息和产品,并分析线上花费带来的效果。
例如,赞助的故事在Facebook新闻传播里的传播;在Instagram里的故事量;在YouTube的视频内容开始前播放的广告;由Outbrain支持的美国有线电视新闻网文章末尾的建议链接,所有这些都是实际使用广告技术的案例。
在过去的一年里,深度学习在数字营销和广告技术中得到了显著地应用。
在这篇文章中,我们将深入探讨一个流行的应用场景的一部分:挖掘网络名人认可的商品。在此过程中,我们将能了解深度学习架构的相对价值,进行实验,理解数据量大小的影响,以及在缺乏足够数据时如何增强数据等内容。
应用场景概述
在本文中,我们将看到如何建立一个深度学习分类器,该分类器可以根据带有商标的图片来预测该商品所对应的公司。本节概述了可以使用此模型的场景。
名人会认可一些产品。通常,他们会在社交媒体上发布图片来炫耀他们认可的品牌。典型帖子会包含一张图片,其中有名人自己和他们写的一些文字。相对应的,品牌的拥有者也渴望了解这些帖子的里他们品牌的展现,并向可能受到影响的潜在客户展示它们。
因此,这一广告技术应用的工作流程如下:将大量的帖子输入处理程序以找出名人、品牌和文字内容。然后,对于每个潜在客户,机器学习模型会根据时间、地点、消息、品牌以及客户的偏好品牌和其他内容生成非常独特的广告。另外一个模型则进行目标客户群的检测。随后进行目标广告的发送。
下图显示了这一工作流程:
名人品牌认可机器人的工作流程。图片由TuhinSharma提供
如你所见,该系统由多个机器学习模型组成。
考虑一下上面所说的图像。这些照片可以是在任何情况下拍摄的。因此首要目标就是确定照片中的物体和名人。这可以通过物体检测模型完成。然后,下一步是识别品牌(如果有的话)。而识别品牌最简单的方法就是通过识别它的商标。
在本文中,我们将研究如何构建一个深度学习模型来通过图像中的商标识别品牌。后续的文章将讨论构建机器人的其他部分(物体检测、文本生成等)。
问题定义
本文中要解决的问题是:给定一张图片,通过标识图片里的商标来预测图片对应的公司(品牌)。
数据
要构建机器学习模型,获取高质量数据集是必须的。在现实业务中,数据科学家会与品牌经理和代理商合作来获得所有可能的商标。
为了本文的目的,我们将利用FlickrLogo数据集。该数据集有来自Flickr(一个流行的照片分享网站)的真实图片。FlickrLogo页面上有关于如何下载数据的说明。如果你想使用本文中的代码构建自己的模型,请自行下载数据。
模型
从商标标识别品牌是一个经典的计算机视觉问题。在过去的几年中,深度学习已成为解决计算机视觉问题的最新技术。因此我们将为我们的场景构建深度学习模型。
软件
在之前的文章中,我们谈到了ApacheMXNet的优点。我们还谈到了Gluon这一基于MXNet的更简单的接口。两者都非常强大,并允许深度学习工程师快速尝试各种模型架构。
现在让我们来看看代码。
我们首先导入构建模型所需的库:
importmxnetasmximportcv2frompathlibimportPathimportosfromtimeimporttimeimportshutilimportmatplotlib.pyplotasplt%matplotlibinline
我们使用FlickrLogos数据集里的FlickrLogos-32数据集。变量flickrlogos-url是这个数据集的URL。
%%capture!wget-ncflickrlogos-url#ReplacewiththeURLtothedataset!unzip-n./FlickrLogos-32_dataset_v2.zip
数据准备
接着是创建下述的数据集:
Train(训练数据集)Validation(验证数据集)Test(测试数据集)
FlickrLogos数据已经分好了训练、验证和测试数据集。下面是数据里图片的信息。
训练数据集包括32个类别,每个类别有10张图片。验证数据集里有张图片,其中张没有包含商标。测试数据有张图片。
所有的训练数据图片都包含有商标,但有些验证和测试数据里的图片没有包含商标。我们是希望构建一个有比较好泛化能力的模型。即我们的模型可以准确地预测它没有见过的图片(验证和测试的图片)。
为了让我们的训练更快速、准确,我们将把50%的没有商标的图片从验证数据集移到训练数据集。这样我们制作出大小为的训练数据集(在从验证数据集添加个无商标图像之后),并将验证数据集减少到张(在移出个无商标图像之后)。在现实生活中,我们应该尝试使用不同的模型架构来选择一个在实际验证和测试数据集上表现良好的模型架构。
下一步,我们定义存储数据的目录。
data_directory=“./FlickrLogos-v2/”
现在定义训练、测试和验证数据列表的路径。对于验证目录,我们定义两个路径:一个存放包含商标的图片,另外一个用于没有商标的图片。
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”
让我们从上面定义的列表里面读入训练、测试和验证(带有商标和无商标的)数据文件名。
从FlickrLogo数据集读入的列表已经被按照训练、测试和验证(包含和未包含商标)进行了分类。
#Listoftrainimageswithopen(train_logos_list_filename)asf:train_logos_filename=f.read().splitlines()#Listofvalidationimageswithoutlogoswithopen(val_nonlogos_list_filename)asf:val_nonlogos_filename=f.read().splitlines()#Listofvalidationimageswithlogoswithopen(val_logos_list_filename)asf:val_logos_filename=f.read().splitlines()#Listoftestimageswithopen(test_list_filename)asf:test_filenames=f.read().splitlines()
现在让我们把一些没有商标的验证图片移动到训练集里面去。这样就让训练数据里包含了原来所有的图片,外加上来自验证数据里50%的没有商标的图片。而验证数据集现在只包含原有的所有带有商标的图片和剩下50%没有商标的图片。
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):]
为了验证我们的数据准备结果是对的,让我们打印训练、测试和验证数据集里的图片数量。
print(“NumberofTrainingImages:“,len(train_filenames))print(“NumberofValidationImages:“,len(val_filenames))print(“NumberofTestingImages:“,len(test_filenames))
数据准备过程的下一步是设置一种目录结构来让模型的训练过程更容易一些。
我们需要目录的结构和下图里的类似。
数据的目录结构。图片由TuhinSharma提供
下面这个函数能帮助我们创建这个目录结构。
defprepare_datesets(base_directory,filenames,dest_folder_name):forfilenameinfilenames:image_src_path=base_directory+filenameimage_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)
使用这个函数来创建训练、验证和测试目录,并把图片按照它们的相应的类别放到目录里面。
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’)
接下来是定义模型所用的超参数。
我们将会有33个类别(32种商标和1个无商标)。这个数据量并不大,所以我们将只会使用一个GPU。我们将会训练20个周期,并使用40作为训练批次的大小。
batch_size=40num_classes=33num_epochs=20num_gpu=1ctx=[mx.gpu(i)foriinrange(num_gpu)]
数据预处理
在图片被导入后,我们需要确保图片的尺寸是一致的。我们会把图片重缩放成*像素大小。
我们有张训练图片,但并不算很多。有没有一个好的办法来获得更多的数据?确实是有的。一张图片在被翻转后依然是表示相同的事物,至少商标还是一样的。被随机剪裁的商标还依然是同一个商标。
因此,我们没有必要为训练来找更多的图片。而是把现有的图片通过翻转和剪切进行一定的变形来获得更多的数据。这同时这还能帮助让模型更加鲁棒。
让我们把50%的训练数据上下翻转,并把它们剪切成*像素大小。
train_augs=[mx.image.HorizontalFlipAug(.5),mx.image.RandomCropAug((,))]
对于验证和测试数据,让我们按中心点剪切图片成*像素大小。现在所有的训练、测试和验证数据集都是*像素大了。
val_test_augs=[mx.image.CenterCropAug((,))]
为了实现对于图片的转换,我们定义了transform函数。这个函数按照输入的数据和增强的类型,对数据进行变换,以更新数据集。
deftransform(data,label,augs):data=data.astype(‘float32’)forauginaugs:data=aug(data)#from(HxWxc)to(cxHxW)data=mx.nd.transpose(data,(2,0,1))returndata,mx.nd.array([label]).asscalar().astype(‘float32’)
Gluon库有一个工具函数可以从文件里导入图片:
mx.gluon.data.vision.ImageFolderDataset
这个函数需要数据按照图2所示的目录结构来存放。
这个函数接收如下的参数:
数据存储的根目录路径一个是否需要把图片转换成灰度图或是彩色图(彩色是默认选项)的标记一个函数来接收数据(图片)和它的标签,并将图片转换。
下面的代码展示了在导入数据后如何对其进行转换:
train_imgs=mx.gluon.data.vision.ImageFolderDataset(data_directory+’train_data’,transform=lambdaX,y:transform(X,y,train_augs))
相同的,对于验证和测试数据集,在导入后也会进行相应的转换。
val_imgs=mx.gluon.data.vision.ImageFolderDataset(data_directory+’val_data’,transform=lambdaX,y:transform(X,y,val_test_augs))test_imgs=mx.gluon.data.vision.ImageFolderDataset(data_directory+’test_data’,transform=lambdaX,y:transform(X,y,val_test_augs))
DataLoader是一个内建的工具函数来从数据集里导入数据,并返回迷你批次的数据。在上述步骤里,我们已经定义了训练、验证和测试数据集(train_imgs、val_imgs、test_imgs相应的)。
num_workers属性让我们可以指定为数据预处理所需的多进程工作器的个数。
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)
现在数据已经导入了,来让我们看一看吧。让我们写一个叫show_images的工具函数来以网格形式显示图片。
defshow_images(imgs,nrows,ncols,figsize=None):“””plotagridofimages”””figsize=(ncols,nrows)_,figs=plt.subplots(nrows,ncols,figsize=figsize)foriinrange(nrows):forjinrange(ncols):figs[j].imshow(imgs[i*ncols+j].asnumpy())figs[j].axes.get_xaxis().set_visible(False)figs[j].axes.get_yaxis().set_visible(False)plt.show()
现在,用4行8列的形式来展示前32张图片。
forX,_intrain_data:#from(BxcxHxW)to(BxHxWxc)X=X.transpose((0,2,3,1)).clip(0,)/show_images(X,4,8)break
进行变形后的图片的网格化展示。图片由TuhinSharma提供
上面代码的运行结果如图3所示。一些图片看起来是含有商标的,不过也经常被切掉了一部分。
用于训练的工具函数
本节内,我们会定义如下一些函数:
在当前处理的批次里获取数据评估模型的准确度训练模型给定URL,获取图片数据对给定的图片,预测图片的标签
第一个函数_get_batch会返回指定批次的数据和标签。
def_get_batch(batch,ctx):“””returndataandlabelonctx”””data,label=batchreturn(mx.gluon.utils.split_and_load(data,ctx),mx.gluon.utils.split_and_load(label,ctx),data.shape[0])
函数evaluate_accuracy会返回模型的分类准确度。针对本文的目的,我们这里选择了一个简单的准确度指标。在实际项目里,准确度指标需要根据应用的需求来设定。
defevaluate_accuracy(data_iterator,net,ctx):acc=mx.nd.array([0])n=0.forbatchindata_iterator:data,label,batch_size=_get_batch(batch,ctx)forX,yinzip(data,label):acc+=mx.nd.sum(net(X).argmax(axis=1)==y).copyto(mx.cpu())n+=y.sizeacc.wait_to_read()returnacc.asscalar()/n
下一个定义的函数是train函数。这是到目前为止我们要创建的最大的函数。
根据给出的模型、训练、测试和验证数据集,模型被按照指定的周期数训练。在之前的一篇文章里,我们对这个函数如何运作进行了更详细的介绍。
一旦在验证数据集上获得了最佳的准确度,这个模型在此检查点的结果会被存下来。在每个周期里,在训练、验证和测试数据集上的准确度都会被打印出来。
deftrain(net,ctx,train_data,val_data,test_data,batch_size,num_epochs,model_prefix,hybridize=False,learning_rate=0.01,wd=0.):net.collect_params().reset_ctx(ctx)ifhybridize==True:net.hybridize()loss=mx.gluon.loss.SoftmaxCrossEntropyLoss()trainer=mx.gluon.Trainer(net.collect_params(),‘sgd’,{‘learning_rate’:learning_rate,‘wd’:wd})best_epoch=-1best_acc=0.0ifisinstance(ctx,mx.Context):ctx=[ctx]forepochinrange(num_epochs):train_loss,train_acc,n=0.0,0.0,0.0start=time()fori,batchinenumerate(train_data):data,label,batch_size=_get_batch(batch,ctx)losses=[]withmx.autograd.record():outputs=[net(X)forXindata]losses=[loss(yhat,y)foryhat,yinzip(outputs,label)]forlinlosses:l.backward()train_loss+=sum([l.sum().asscalar()forlinlosses])trainer.step(batch_size)n+=batch_sizetrain_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,Trainacc%.2f,Valacc%.2f,Testacc%.2f,Time%.1fsec”%(epoch,train_loss/n,train_acc,val_acc,test_acc,time()–start))ifval_accbest_acc:best_acc=val_accifbest_epoch!=-1:print(‘Deletingpreviouscheckpoint…’)os.remove(model_prefix+’-%d.params’%(best_epoch))best_epoch=epochprint(‘Bestvalidationaccuracyfound.Checkpointing…’)net.collect_params().save(model_prefix+’-%d.params’%(epoch))
函数get_image会根据给定的URL返回一个图片。这个函数可以用来测试模型的准确度。
defget_image(url,show=False):#downloadandshowtheimagefname=mx.test_utils.download(url)img=cv2.cvtColor(cv2.imread(fname),cv2.COLOR_BGR2RGB)img=cv2.resize(img,(,))plt.imshow(img)returnfname
最后一个工具函数是classify_logo。给定图片和模型,这个函数将会返回此图片的分类(在我们的场景里就是品牌的名字)和此分类相应的概率。
defclassify_logo(net,url):fname=get_image(url)withopen(fname,‘rb’)asf: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.synsetsreturn‘Withprob=%f,%s’%(prob,label[pred])
模型
理解模型的架构是非常重要的。在我们之前的那篇文章里,我们构建了一个多层感知机(MLP)。此架构如图4所示。
多层感知机。图片由TuhinSharma提供
此MLP模型的输入层应该是怎么样的?我的数据图片的尺寸是*像素。
构建输入层的最常见的方法就是把图片打平,构建一个个(*)神经元的输入层。这就形成了一个下图所示的简单的数据流。
扁平化输入。图片由TuhinSharma提供
但是当进行这样的扁平化处理后,图像数据里的很多空间信息被丢失了。同时另外一个挑战是相应的权重数量。如果第一个隐藏层有30个神经元,那么这个模型的参数将会有*30再加上30个偏置量。因此,这看来不像是一个好的为图像建模的方法。
现在让我们来讨论一下一个更合适的架构:用于图片分类的卷积神经网络(CNN)。
卷积神经网络(CNN)
CNN和MLP类似,因为它也是构建神经网络并为神经元学习权重。CNN和MLP的关键的区别是输入数据是图片。CNN允许我们在架构里充分利用图片的特性。
CNN有一些卷积层。这个词汇“卷积”是来自图像处理领域,如图6所述。它工作于一个较小的窗口,叫做“感知域”,而不是处理来自前一层的所有输入。这种机制就可以让模型学习局部的特征。
每个卷积层用一个小矩阵(叫卷积核)在进入本层的图像上面的一部分上移动。卷积会对卷积矩阵内的每个像素进行修改,此运算可以帮助识别边缘。图6的左边展示了一个图片,中间是一个3×3的卷积核,而运用此卷积核对左边图片的左上角像素计算的结果显示在右边图里。我们还能定义多个卷积核,来表示不同的特征图。
卷积层。图片由TuhinSharma提供
在上图的例子里,输入的图片的尺寸是5×5,而卷积核的尺寸是3×3。卷积计算是两个矩阵的元素与元素的乘积之和。例子里卷积的输出尺寸也是5×5。
为了理解这些,我们需要理解卷积层里的两个重要参数:步长(stride)和填充方法(padding)。
步长控制卷积核(过滤器)如何在图片上移动。
下图表明了卷积核从第一个像素到第二个像素的移动过程。
卷积核的移动。图片由TuhinSharma提供
在上图中,步长是1。
当对一个5×5的图片进行3×3的卷积计算后,我们将得到一个3×3的图片。针对这一情况,我们会在图片的边缘进行填充。现在这个5×5的图片被0所围绕,如下图所示。
用0填充边缘。图片由TuhinSharma提供
这样,当我们用3×3卷积核计算时,将会获得一个5×5的输出。
因此对于上图所示的计算,它的步长是1,且填充的尺寸也是1。
CNN比相应的MLP能极大地减少权重的数量。假设我们使用30个卷积核,每个是3×3。每个卷积核的参数是3×3=9,外加1个偏置量。这样每个卷积核有10个权重,总共30个卷积核就是个权重。而在前面的章节里,MLP则是有00个权重。
下一层一般典型地是一个子抽样层。一旦我们识别了特征,这一子抽样层会简化这个信息。一个常用的方法是最大池化。它从卷积层输出的局部区域输出最大值(见下图)。这一层在保留了每个局部区域的最大激活特征的同时,降低了输出的尺寸。
最大池化。图片由TuhinSharma提供
可以看到最大池化在保留了每个局部区域的最大激活特征的同时,降低了输出的尺寸。
想了解关于CNN的更多的信息,一个好的资源是这本在线图书《神经网络和深度学习》。另外一个好的资源是斯坦福大学的CNN课程。
现在我们已经对什么是CNN有了基本的了解。为了这里的问题,让我们用gluon来实现它。
第一步是定义这个架构:
cnn_net=mx.gluon.nn.Sequential()withcnn_net.name_scope():#Firstconvolutionallayercnn_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))#Secondconvolutionallayercnn_net.add(mx.gluon.nn.Conv2D(channels=,kernel_size=5,activation=’relu’))cnn_net.add(mx.gluon.nn.MaxPool2D(pool_size=3,strides=(2,2)))#Flattenandapplyfulllyconnectedlayerscnn_net.add(mx.gluon.nn.Flatten())cnn_net.add(mx.gluon.nn.Dense(,activation=”relu”))cnn_net.add(mx.gluon.nn.Dense(num_classes))
在模型架构被定义好之后,让我们初始化网络里的权重。我们将使用Xavier初始器。
cnn_net.collect_params().initialize(mx.init.Xavier(magnitude=2.24),ctx=ctx)
权重初始化完后,我们可以训练模型了。我们会调用之前定义的相同的train函数,并传给它所需的参数。
train(cnn_net,ctx,train_data,val_data,test_data,batch_size,num_epochs,model_prefix=’cnn’)Epoch0.Loss:53.,Trainacc0.77,Valacc0.58,Testacc0.72,Time.9secBestvalidationaccuracyfound.Checkpointing…Epoch1.Loss:3.,Trainacc0.80,Valacc0.60,Testacc0.73,Time.7secDeletingpreviouscheckpoint…Bestvalidationaccuracyfound.Checkpointing…Epoch2.Loss:3.,Trainacc0.81,Valacc0.60,Testacc0.74,Time.5secDeletingpreviouscheckpoint…Bestvalidationaccuracyfound.Checkpointing…Epoch3.Loss:3.,Trainacc0.82,Valacc0.61,Testacc0.75,Time.4secDeletingpreviouscheckpoint…Bestvalidationaccuracyfound.Checkpointing…Epoch4.Loss:3.,Trainacc0.82,Valacc0.61,Testacc0.75,Time.0secDeletingpreviouscheckpoint…Bestvalidationaccuracyfound.Checkpointing…Epoch5.Loss:2.,Trainacc0.82,Valacc0.61,Testacc0.76,Time.7secDeletingpreviouscheckpoint…Bestvalidationaccuracyfound.Checkpointing…Epoch6.Loss:2.,Trainacc0.82,Valacc0.61,Testacc0.76,Time.1secEpoch7.Loss:1.,Trainacc0.82,Valacc0.61,Testacc0.76,Time.3secEpoch8.Loss:1.,Trainacc0.82,Valacc0.61,Testacc0.76,Time.6secEpoch9.Loss:1.,Trainacc0.82,Valacc0.61,Testacc0.76,Time.5secEpoch10.Loss:1.,Trainacc0.82,Valacc0.61,Testacc0.76,Time.5secEpoch11.Loss:1.,Trainacc0.82,Valacc0.61,Testacc0.76,Time.7secEpoch12.Loss:1.,Trainacc0.82,Valacc0.61,Testacc0.76,Time.1secEpoch13.Loss:1.,Trainacc0.82,Valacc0.61,Testacc0.76,Time.3secEpoch14.Loss:1.,Trainacc0.82,Valacc0.61,Testacc0.76,Time.3secEpoch15.Loss:1.,Trainacc0.82,Valacc0.61,Testacc0.76,Time.4secEpoch16.Loss:1.,Trainacc0.82,Valacc0.61,Testacc0.76,Time.3secEpoch17.Loss:1.,Trainacc0.82,Valacc0.61,Testacc0.76,Time.8secEpoch18.Loss:1.,Trainacc0.82,Valacc0.61,Testacc0.76,Time.8secEpoch19.Loss:1.,Trainacc0.82,Valacc0.61,Testacc0.76,Time.9sec
我们让模型运行20个周期。典型的情况是,我们会训练非常多的周期,并选择验证准确度最高的那个模型。在上面运行了20个周期后,我们可以在日志里看到验证准确度最高的是在周期5。
在此周期之后,模型看起来并没有学到更多。有可能网络已经饱和,学习速度变慢了。我们下一节里会试一个更好的方法,但还是先让我们看看现在这个模型的表现如何。
让我们把最佳验证准确度的模型参数导入,然后分配给我们的模型:
cnn_net.collect_params().load(‘cnn-%d.params’%(5),ctx)
现在让我们看看这个模型在新数据上的表现。我们会从网上获取一个容易识别的图片(见下图),并看看模型是否能准确地识别。
img_url=“