使用Flux.jl进行图像分类

人工智能等各类编程培训资料整理,所有资源无秘无压缩-购买会员

在PyTorch从事一个项目,这个项目创建一个深度学习模型,可以检测未知物种的疾病。

最近,决定在Julia中重建这个项目,并将其用作学习Flux.jl[1]的练习,这是Julia最流行的深度学习包(至少在GitHub上按星级排名)。

但在这样做的过程中,遇到了一些挑战,这些挑战在网上或文档中找不到好的例子。因此,决定写这篇文章,作为其他任何想在Flux做类似事情的人的参考资料。

这是给谁的?

因为Flux.jl(以下简称为“Flux”)是一个深度学习包,所以我主要为熟悉深度学习概念(如迁移学习)的读者编写这篇文章。

虽然在写这篇文章时也考虑到了Flux的一个半新手(比如我自己),但其他人可能会觉得这很有价值。只是要知道,写这篇文章并不是对Julia或通量的全面介绍或指导。为此,将分别参考其他资源,如官方的Julia和Flux文档。

最后,对PyTorch做了几个比较。了解本文观点并不需要有PyTorch的经验,但有PyTorch经验的人可能会觉得它特别有趣。

为什么是Julia?为什么选择Flux.jl?

如果你已经使用了Julia和/或Flux,你可能可以跳过本节。此外,许多其他人已经写了很多关于这个问题的帖子,所以我将简短介绍。

归根结底,我喜欢Julia。它在数值计算方面很出色,编程时真的很开心,而且速度很快。原生快速:不需要NumPy或其他底层C++代码的包装器。

至于为什么选择Flux,是因为它是Julia中最流行的深度学习框架,用纯Julia编写,可与Julia生态系统组合。

项目本身

好吧,既然我已经无耻地说服了Julia,现在是时候了解项目本身的信息了。

我使用了三个数据集——PlantVillage[2]、PlantLeaves[3]和PlantaeK[4]——涵盖了许多不同的物种。

我使用PlantVillage作为训练集,其他两个组合作为测试集。这意味着模型必须学习一些可以推广到未知物种的知识,因为测试集将包含未经训练的物种。

了解到这一点,我创建了三个模型:

使用ResNet迁移学习的基线

具有自定义CNN架构的孪生(又名暹罗)神经网络

具有迁移学习的孪生神经网络

本文的大部分内容将详细介绍处理数据、创建和训练模型的一些挑战和痛点。

处理数据

第一个挑战是数据集的格式错误。我不会在这里详细介绍如何对它们进行预处理,但最重要的是我创建了两个图像目录,即训练和测试。

这两个文件都填充了一长串图像,分别命名为img0.jpg、img1.jpg、imm2.jpg等。我还创建了两个CSV,一个用于训练集,一个为测试集,其中一列包含文件名,一列包含标签。

上述结构很关键,因为数据集的总容量超过10 GB,我电脑的内存肯定无法容纳,更不用说GPU的内存了。因此,我们需要使用DataLoader。(如果你曾经使用过PyTorch,你会很熟悉;这里的概念与PyTorch基本相同。)

为了在Flux中实现这一点,我们需要创建一个自定义结构来包装我们的数据集,以允许它批量加载数据。

为了让我们的自定义结构能够构造数据加载器,我们需要做的就是为类型定义两个方法:length和getindex。下面是我们将用于数据集的实现:

usingFlux

usingImages

usingFileIO

usingDataFrames

usingPipe

“””

ImageDataContainer(labels_df,img_dir)

Implementsthefunctions`length`and`getindex`,whicharerequiredtouseImageDataContainer

asanargumentinaDataLoaderforFlux.

“””

structImageDataContainer

labels::AbstractVector

filenames::AbstractVector{String}

functionImageDataContainer(labels_df::DataFrame,img_dir::AbstractString)

filenames=img_dir.*labels_df[!,1]#firstcolumnshouldbethefilenames

labels=labels_df[!,2]#secondcolumnshouldbethelabels

returnnew(labels,filenames)

end

end

“Getsthenumberofobservationsforagivendataset.”

functionBase.length(dataset::ImageDataContainer)

returnlength(dataset.labels)

end

“Getsthei-thtoj-thobservations(includinglabels)foragivendataset.”

functionBase.getindex(dataset::ImageDataContainer,idxs::Union{UnitRange,Vector})

batch_imgs=map(idx->load(dataset.filenames[idx]),idxs)

batch_labels=map(idx->dataset.labels[idx],idxs)

“AppliesnecessarytransformsandreshapingstobatchesandloadsthemontoGPUtobefedintoamodel.”

functiontransform_batch(imgs,labels)

#convertimgsto256×256×3×64array(Height×Width×Color×Number)offloats(valuesbetween0.0and1.0)

#arraysneedtobesenttogpuinsidetrainingloopforgarbagecollectortoworkproperly

batch_X=@pipehcat(imgs…)>reshape(_,(HEIGHT,WIDTH,length(labels)))>channelview>permutedims(_,(2,3,1,4))

batch_y=@pipelabels>reshape(_,(1,length(labels)))

return(batch_X,batch_y)

end

returntransform_batch(batch_imgs,batch_labels)

end

本质上,当Flux试图检索一批图像时,它会调用getindex(dataloader, i:i+batchsize),这在Julia中相当于dataloader[i:i+batchsize]。

因此,我们的自定义getindex函数获取文件名列表,获取适当的文件名,加载这些图像,然后将其处理并重新塑造为适当的HEIGHT × WIDTH × COLOR × NUMBER形状。标签也是如此。

然后,我们的训练、验证和测试数据加载器可以非常容易地完成:

usingFlux:Data.DataLoader

usingCSV

usingDataFrames

usingMLUtils

#dataframescontainingfilenamesforimagesandcorrespondinglabels

consttrain_df=DataFrame(CSV.File(dataset_dir*”train_labels.csv”))

consttest_df=DataFrame(CSV.File(dataset_dir*”test_labels.csv”))

#ImageDataContainerwrappersfordataframes

#givesinterfaceforgettingtheactualimagesandlabelsastensors

consttrain_dataset=ImageDataContainer(train_df,train_dir)

consttest_dataset=ImageDataContainer(test_df,test_dir)

#randomlysorttraindatasetintotrainingandvalidationsets

consttrain_set,val_set=splitobs(train_dataset,at=0.7,shuffle=true)

consttrain_loader=DataLoader(train_set,batchsize=BATCH_SIZE,shuffle=true)

constval_loader=DataLoader(val_set,batchsize=BATCH_SIZE,shuffle=true)

consttest_loader=DataLoader(test_dataset,batchsize=BATCH_SIZE)

制作模型

数据加载器准备就绪后,下一步是创建模型。首先是基于ResNet的迁移学习模型。事实证明,这项工作相对困难。

在Metalhead.jsl包中(包含用于迁移学习的计算机视觉Flux模型),创建具有预训练权重的ResNet18模型应该与model = ResNet(18; pretrain = true)一样简单。

然而,至少在编写本文时,创建预训练的模型会导致错误。这很可能是因为Metalhead.jsl仍在添加预训练的权重。

我终于在HuggingFace上找到了包含权重的.tar.gz文件:

https://huggingface.co/FluxML/resnet18

我们可以使用以下代码加载权重,并创建我们自己的自定义Flux模型:

usingFlux

usingMetalhead

usingPipe

usingBSON

#loadinsavedparamsfrombson

resnet=ResNet(18)

@pipejoinpath(@__DIR__,”resnet18.bson”)>BSON.load(_)[:model]>Flux.loadmodel!(resnet,_)

#lastelementofresnet18isachain

#sincewereremovingthelastelement,wejustwanttorecreateit,butwithdifferentnumberofclasses

#probablyamoreelegant,lesshard-codedwaytodothis,butwhatever

baseline_model=Chain(

resnet.layers[1:end-1],

Chain(

AdaptiveMeanPool((1,1)),

Flux.flatten,

Dense(512=>N_CLASSES)

)

)

(注意:如果有比这更优雅的方法来更改ResNet的最后一层,请告诉我。)

创建了预训练的迁移学习模型后,这只剩下两个孪生网络模型。然而,与迁移学习不同,我们必须学习如何手动创建模型。(如果你习惯了PyTorch,这就是Flux与PyTorch的不同之处。)

使用Flux文档和其他在线资源创建CNN相对容易。然而,Flux没有内置层来表示具有参数共享的Twin网络。它最接近的是平行层,它不使用参数共享。

然而,Flux在这里有关于如何创建自定义多个输入或输出层的文档。在我们的例子中,我们可以用来创建自定义Twin层的代码如下:

usingFlux

“CustomFluxNNlayerwhichwillcreatetwinnetworkfrom`path`withsharedparametersandcombinetheiroutputwith`combine`.”

structTwin{T,F}

combine::F

path::T

end

#definetheforwardpassoftheTwinlayer

#feedsbothinputs,X,throughthesamepath(i.e.,sharedparameters)

#andcombinestheiroutputs

Flux.@functorTwin

(m::Twin)(Xs::Tuple)=m.combine(map(X->m.path(X),Xs)…)

首先请注意,它以一个简单的结构Twin开头,包含两个字段:combine和path。path是我们的两个图像输入将经过的网络,而combine是在最后将输出组合在一起的函数。

使用Flux.@functor告诉Flux将我们的结构像一个常规的Flux层一样对待。(m::Twin)(Xs::Tuple) = m.combine(map(X -> m.path(X), Xs)…)定义了前向传递,其中元组Xs中的所有输入X都通过path馈送,然后所有输出都通过combine。

要使用自定义CNN架构创建Twin网络,我们可以执行以下操作:

usingFlux

twin_model=Twin(

#thislayercombinestheoutputsofthetwinCNNs

Flux.Bilinear((32,32)=>1),

#thisisthearchitecturethatformsthepathofthetwinnetwork

Chain(

#layer1

Conv((5,5),3=>18,relu),

MaxPool((3,3),stride=3),

#layer2

Conv((5,5),18=>36,relu),

MaxPool((2,2),stride=2),

#layer3

Conv((3,3),36=>72,relu),

MaxPool((2,2),stride=2),

Flux.flatten,

#layer4

Dense(19*19*72=>64,relu),

#Dropout(0.1),

#outputlayer

Dense(64=>32,relu)

)

)

在本例中,我们实际上使用Flux.Biliner层作为组合,这实质上创建了一个连接到两个独立输入的输出层。上面,两个输入是路径的输出,即自定义CNN架构。或者,我们可以以某种方式使用hcat或vcat作为组合,然后在最后添加一个Dense层,但这个解决方案似乎更适合这个问题。

现在,要使用ResNet创建Twin网络,我们可以执行以下操作:

usingFlux

usingMetalhead

usingPipe

usingBSON

#loadinsavedparamsfrombson

resnet=ResNet(18)

@pipejoinpath(@__DIR__,”resnet18.bson”)>BSON.load(_)[:model]>Flux.loadmodel!(resnet,_)

#createtwinresnetmodel

twin_resnet=Twin(

Flux.Bilinear((32,32)=>1),

Chain(

resnet.layers[1:end-1],

Chain(

AdaptiveMeanPool((1,1)),

Flux.flatten,

Dense(512=>32)

)

)

)

请注意,我们如何使用与之前相同的技巧,并使用Flux.双线性层作为组合,并使用与之前类似的技巧来使用预训练的ResNet作为路径。

训练时间

现在我们的数据加载器和模型准备就绪,剩下的就是训练了。通常,在Flux中,可以使用一个简单的一行代码,@epochs 2 Flux.train!(loss, ps, dataset, opt),但我们确实有一些定制的事情要做。

首先,非孪生网络的训练循环:

usingFlux

usingFlux:Losses.logitbinarycrossentropy

usingCUDA

usingProgressLogging

usingPipe

usingBSON

“Storesthehistorythroughalltheepochsofkeytraining/validationperformancemetrics.”

mutablestructTrainingMetrics

val_acc::Vector{AbstractFloat}

val_loss::Vector{AbstractFloat}

TrainingMetrics(n_epochs::Integer)=new(zeros(n_epochs),zeros(n_epochs))

end

“Trainsgivenmodelforagivennumberofepochsandsavesthemodelthatperformsbestonthevalidationset.”

functiontrain!(model,n_epochs::Integer,filename::String)

model=model>gpu

optimizer=ADAM()

params=Flux.params(model[end])#transferlearning,soonlytraininglastlayers

metrics=TrainingMetrics(n_epochs)

#zeroinitperformancemeasuresforepoch

epoch_acc=0.0

epoch_loss=0.0

#sowecanautomaticallysavethemodelwithbestvalaccuracy

best_acc=0.0

#Xandyarealreadyintherightshapeandonthegpu

#iftheywerent,Zygote.jlwouldthrowafitbecauseitneedstobeabletodifferentiatethisfunction

loss(X,y)=logitbinarycrossentropy(model(X),y)

@info”Beginningtrainingloop…”

forepoch_idx∈1:n_epochs

@info”Trainingepoch$(epoch_idx)…”

#train1epoch,recordperformance

@withprogressfor(batch_idx,(imgs,labels))∈enumerate(train_loader)

X=@pipeimgs>gpu>float32.(_)

y=@pipelabels>gpu>float32.(_)

gradients=gradient(()->loss(X,y),params)

Flux.Optimise.update!(optimizer,params,gradients)

@logprogressbatch_idx/length(enumerate(train_loader))

end

#resetvariables

epoch_acc=0.0

epoch_loss=0.0

@info”Validatingepoch$(epoch_idx)…”

#val1epoch,recordperformance

@withprogressfor(batch_idx,(imgs,labels))∈enumerate(val_loader)

X=@pipeimgs>gpu>float32.(_)

y=@pipelabels>gpu>float32.(_)

#feedthroughthemodeltocreateprediction

ŷ=model(X)

#calculatethelossandaccuracyforthisbatch,addtoaccumulatorforepochresults

batch_acc=@pipe((((σ.(ŷ).>0.5).*1.0).==y).*1.0)>cpu>reduce(+,_)

epoch_acc+=batch_acc

batch_loss=logitbinarycrossentropy(ŷ,y)

epoch_loss+=(batch_loss>cpu)

@logprogressbatch_idx/length(enumerate(val_loader))

end

#addaccandlosstolists

metrics.val_acc[epoch_idx]=epoch_acc/length(val_set)

metrics.val_loss[epoch_idx]=epoch_loss/length(val_set)

#automaticallysavethemodeleverytimeitimprovesinvalaccuracy

ifmetrics.val_acc[epoch_idx]>=best_acc

@info”Newbestaccuracy:$(metrics.val_acc[epoch_idx])!Savingmodeloutto$(filename).bson”

BSON.@savejoinpath(@__DIR__,”$(filename).bson”)

best_acc=metrics.val_acc[epoch_idx]

end

end

returnmodel,metrics

end

这里有很多要解开的东西,但本质上这做了一些事情:

它创建了一个结构,用于跟踪我们想要的任何验证度量。在这种情况下是每个epoch的损失和精度。

它只选择要训练的最后一层参数。如果我们愿意,我们可以训练整个模型,但这在计算上会更费力。这是不必要的,因为我们使用的是预训练的权重。

对于每个epoch,它都会遍历要训练的训练集的所有批次。然后,它计算整个验证集(当然是成批的)的准确性和损失。如果提高了epoch的验证精度,则可以保存模型。如果没有,它将继续到下一个时代。

请注意,我们可以在这里做更多的工作,例如,提前停止,但以上内容足以了解大致情况。

接下来,Twin网络的训练循环非常相似,但略有不同:

usingFlux

usingFlux:Losses.logitbinarycrossentropy

usingCUDA

usingProgressLogging

usingPipe

usingBSON

“Trainsgiventwinmodelforagivennumberofepochsandsavesthemodelthatperformsbestonthevalidationset.”

functiontrain!(model::Twin,n_epochs::Integer,filename::String;is_resnet::Bool=false)

model=model>gpu

optimizer=ADAM()

params=is_resnet?Flux.params(model.path[end:end],model.combine):Flux.params(model)#ifcustomCNN,needtotrainallparams

metrics=TrainingMetrics(n_epochs)

#zeroinitperformancemeasuresforepoch

epoch_acc=0.0

epoch_loss=0.0

#sowecanautomaticallysavethemodelwithbestvalaccuracy

best_acc=0.0

#Xandyarealreadyintherightshapeandonthegpu

#iftheywerent,Zygote.jlwouldthrowafitbecauseitneedstobeabletodifferentiatethisfunction

loss(Xs,y)=logitbinarycrossentropy(model(Xs),y)

@info”Beginningtrainingloop…”

forepoch_idx∈1:n_epochs

@info”Trainingepoch$(epoch_idx)…”

#train1epoch,recordperformance

@withprogressfor(batch_idx,((imgs₁,labels₁),(imgs₂,labels₂)))∈enumerate(zip(train_loader₁,train_loader₂))

X₁=@pipeimgs₁>gpu>float32.(_)

y₁=@pipelabels₁>gpu>float32.(_)

X₂=@pipeimgs₂>gpu>float32.(_)

y₂=@pipelabels₂>gpu>float32.(_)

Xs=(X₁,X₂)

y=((y₁.==y₂).*1.0)#yrepresentsifbothimageshavethesamelabel

gradients=gradient(()->loss(Xs,y),params)

Flux.Optimise.update!(optimizer,params,gradients)

@logprogressbatch_idx/length(enumerate(train_loader₁))

end

#resetvariables

epoch_acc=0.0

epoch_loss=0.0

@info”Validatingepoch$(epoch_idx)…”

#val1epoch,recordperformance

@withprogressfor(batch_idx,((imgs₁,labels₁),(imgs₂,labels₂)))∈enumerate(zip(val_loader₁,val_loader₂))

X₁=@pipeimgs₁>gpu>float32.(_)

y₁=@pipelabels₁>gpu>float32.(_)

X₂=@pipeimgs₂>gpu>float32.(_)

y₂=@pipelabels₂>gpu>float32.(_)

Xs=(X₁,X₂)

y=((y₁.==y₂).*1.0)#yrepresentsifbothimageshavethesamelabel

#feedthroughthemodeltocreateprediction

ŷ=model(Xs)

#calculatethelossandaccuracyforthisbatch,addtoaccumulatorforepochresults

batch_acc=@pipe((((σ.(ŷ).>0.5).*1.0).==y).*1.0)>cpu>reduce(+,_)

epoch_acc+=batch_acc

batch_loss=logitbinarycrossentropy(ŷ,y)

epoch_loss+=(batch_loss>cpu)

@logprogressbatch_idx/length(enumerate(val_loader))

end

#addaccandlosstolists

metrics.val_acc[epoch_idx]=epoch_acc/length(val_set)

metrics.val_loss[epoch_idx]=epoch_loss/length(val_set)

#automaticallysavethemodeleverytimeitimprovesinvalaccuracy

ifmetrics.val_acc[epoch_idx]>=best_acc

@info”Newbestaccuracy:$(metrics.val_acc[epoch_idx])!Savingmodeloutto$(filename).bson”

BSON.@savejoinpath(@__DIR__,”$(filename).bson”)

best_acc=metrics.val_acc[epoch_idx]

end

end

returnmodel,metrics

end

首先注意,我们使用了一个同名函数train!,但具有稍微不同的函数签名。这允许Julia根据我们正在训练的网络类型来分配正确的功能。

还要注意,Twin ResNet模型冻结其预训练的参数,而我们训练所有Twin自定义CNN参数。

除此之外,训练循环的其余部分基本相同,只是我们必须使用两个训练数据加载器和两个验证数据加载器。这些为我们提供了两个输入和每批两组标签,我们将其适当地输入到Twin模型中。

最后,请注意,Twin模型预测两个输入图像是否具有相同的标签,而常规非Twin网络仅直接预测标签。

这样,为所有三个模型的测试集构建测试循环应该不会太难。因为这篇文章的目的是要解决我在网上找不到例子的主要痛点,所以我将把测试部分作为练习留给读者。

最后

最大的挑战是缩小从相对简单的示例到更先进的技术之间的差距,而这些技术缺乏示例。但这也揭示了Julia的优势:因为它本身就很快,所以搜索包的源代码以找到答案通常非常容易。

有几次,我发现自己在浏览Flux源代码,以了解一些东西是如何工作的。每一次我都能非常轻松快速地找到答案。我不确定我是否有勇气为PyTorch尝试类似的东西。

另一个挑战是Metalhead.jsl的不成熟状态,这在Julia生态系统中肯定不是独一无二的,因为它的功能不完整。

最后一个想法是,我发现Flux非常有趣和优雅……一旦我掌握了它的窍门。我肯定会在未来与Flux一起进行更深入的学习。

感谢阅读!

参考引用

[1] M. Innes, Flux: Elegant Machine Learning with Julia (2018), Journal of Open Source Software

[2] Arun Pandian J. and G. Gopal, Data for: Identification of Plant Leaf Diseases Using a 9-layer Deep Convolutional Neural Network (2019), Mendeley Data

[3] S. S. Chouhan, A. Kaul, and U. P. Singh, A Database of Leaf Images: Practice towards Plant Conservation with Plant Pathology (2019), Mendely Data

[4] V. P. Kour and S. Arora, PlantaeK: A leaf database of native plants of Jammu and Kashmir (2019), Mendeley Data

免责声明: 1、本站信息来自网络,版权争议与本站无关 2、本站所有主题由该帖子作者发表,该帖子作者与本站享有帖子相关版权 3、其他单位或个人使用、转载或引用本文时必须同时征得该帖子作者和本站的同意 4、本帖部分内容转载自其它媒体,但并不代表本站赞同其观点和对其真实性负责 5、用户所发布的一切软件的解密分析文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。 6、您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。 7、请支持正版软件、得到更好的正版服务。 8、如有侵权请立即告知本站,本站将及时予与删除 9、本站所发布的一切破解补丁、注册机和注册信息及软件的解密分析文章和视频仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。
600学习网 » 使用Flux.jl进行图像分类