使用Flux.jl進(jìn)行圖像分類
在PyTorch從事一個(gè)項(xiàng)目,這個(gè)項(xiàng)目創(chuàng)建一個(gè)深度學(xué)習(xí)模型,可以檢測(cè)未知物種的疾病。
最近,決定在Julia中重建這個(gè)項(xiàng)目,并將其用作學(xué)習(xí)Flux.jl[1]的練習(xí),這是Julia最流行的深度學(xué)習(xí)包(至少在GitHub上按星級(jí)排名)。
但在這樣做的過程中,遇到了一些挑戰(zhàn),這些挑戰(zhàn)在網(wǎng)上或文檔中找不到好的例子。因此,決定寫這篇文章,作為其他任何想在Flux做類似事情的人的參考資料。
這是給誰的?
因?yàn)镕lux.jl(以下簡(jiǎn)稱為“Flux”)是一個(gè)深度學(xué)習(xí)包,所以我主要為熟悉深度學(xué)習(xí)概念(如遷移學(xué)習(xí))的讀者編寫這篇文章。
雖然在寫這篇文章時(shí)也考慮到了Flux的一個(gè)半新手(比如我自己),但其他人可能會(huì)覺得這很有價(jià)值。只是要知道,寫這篇文章并不是對(duì)Julia或通量的全面介紹或指導(dǎo)。為此,將分別參考其他資源,如官方的Julia和Flux文檔。
最后,對(duì)PyTorch做了幾個(gè)比較。了解本文觀點(diǎn)并不需要有PyTorch的經(jīng)驗(yàn),但有PyTorch經(jīng)驗(yàn)的人可能會(huì)覺得它特別有趣。
為什么是Julia?為什么選擇Flux.jl?
如果你已經(jīng)使用了Julia和/或Flux,你可能可以跳過本節(jié)。此外,許多其他人已經(jīng)寫了很多關(guān)于這個(gè)問題的帖子,所以我將簡(jiǎn)短介紹。
歸根結(jié)底,我喜歡Julia。它在數(shù)值計(jì)算方面很出色,編程時(shí)真的很開心,而且速度很快。原生快速:不需要NumPy或其他底層C++代碼的包裝器。
至于為什么選擇Flux,是因?yàn)樗荍ulia中最流行的深度學(xué)習(xí)框架,用純Julia編寫,可與Julia生態(tài)系統(tǒng)組合。
項(xiàng)目本身
好吧,既然我已經(jīng)無恥地說服了Julia,現(xiàn)在是時(shí)候了解項(xiàng)目本身的信息了。
我使用了三個(gè)數(shù)據(jù)集——PlantVillage[2]、PlantLeaves[3]和PlantaeK[4]——涵蓋了許多不同的物種。
我使用PlantVillage作為訓(xùn)練集,其他兩個(gè)組合作為測(cè)試集。這意味著模型必須學(xué)習(xí)一些可以推廣到未知物種的知識(shí),因?yàn)闇y(cè)試集將包含未經(jīng)訓(xùn)練的物種。
了解到這一點(diǎn),我創(chuàng)建了三個(gè)模型:
使用ResNet遷移學(xué)習(xí)的基線
具有自定義CNN架構(gòu)的孿生(又名暹羅)神經(jīng)網(wǎng)絡(luò)
具有遷移學(xué)習(xí)的孿生神經(jīng)網(wǎng)絡(luò)
本文的大部分內(nèi)容將詳細(xì)介紹處理數(shù)據(jù)、創(chuàng)建和訓(xùn)練模型的一些挑戰(zhàn)和痛點(diǎn)。
處理數(shù)據(jù)
第一個(gè)挑戰(zhàn)是數(shù)據(jù)集的格式錯(cuò)誤。我不會(huì)在這里詳細(xì)介紹如何對(duì)它們進(jìn)行預(yù)處理,但最重要的是我創(chuàng)建了兩個(gè)圖像目錄,即訓(xùn)練和測(cè)試。
這兩個(gè)文件都填充了一長(zhǎng)串圖像,分別命名為img0.jpg、img1.jpg、imm2.jpg等。我還創(chuàng)建了兩個(gè)CSV,一個(gè)用于訓(xùn)練集,一個(gè)為測(cè)試集,其中一列包含文件名,一列包含標(biāo)簽。
上述結(jié)構(gòu)很關(guān)鍵,因?yàn)閿?shù)據(jù)集的總?cè)萘砍^10 GB,我電腦的內(nèi)存肯定無法容納,更不用說GPU的內(nèi)存了。因此,我們需要使用DataLoader。(如果你曾經(jīng)使用過PyTorch,你會(huì)很熟悉;這里的概念與PyTorch基本相同。)
為了在Flux中實(shí)現(xiàn)這一點(diǎn),我們需要?jiǎng)?chuàng)建一個(gè)自定義結(jié)構(gòu)來包裝我們的數(shù)據(jù)集,以允許它批量加載數(shù)據(jù)。
為了讓我們的自定義結(jié)構(gòu)能夠構(gòu)造數(shù)據(jù)加載器,我們需要做的就是為類型定義兩個(gè)方法:length和getindex。下面是我們將用于數(shù)據(jù)集的實(shí)現(xiàn):
using Flux
using Images
using FileIO
using DataFrames
using Pipe
"""
ImageDataContainer(labels_df, img_dir)
Implements the functions `length` and `getindex`, which are required to use ImageDataContainer
as an argument in a DataLoader for Flux.
"""
struct ImageDataContainer
labels::AbstractVector
filenames::AbstractVector{String}
function ImageDataContainer(labels_df::DataFrame, img_dir::AbstractString)
filenames = img_dir .* labels_df[!, 1] # first column should be the filenames
labels = labels_df[!, 2] # second column should be the labels
return new(labels, filenames)
end
end
"Gets the number of observations for a given dataset."
function Base.length(dataset::ImageDataContainer)
return length(dataset.labels)
end
"Gets the i-th to j-th observations (including labels) for a given dataset."
function Base.getindex(dataset::ImageDataContainer, idxs::Union{UnitRange,Vector})
batch_imgs = map(idx -> load(dataset.filenames[idx]), idxs)
batch_labels = map(idx -> dataset.labels[idx], idxs)
"Applies necessary transforms and reshapings to batches and loads them onto GPU to be fed into a model."
function transform_batch(imgs, labels)
# convert imgs to 256×256×3×64 array (Height×Width×Color×Number) of floats (values between 0.0 and 1.0)
# arrays need to be sent to gpu inside training loop for garbage collector to work properly
batch_X = @pipe hcat(imgs...) |> reshape(_, (HEIGHT, WIDTH, length(labels))) |> channelview |> permutedims(_, (2, 3, 1, 4))
batch_y = @pipe labels |> reshape(_, (1, length(labels)))
return (batch_X, batch_y)
end
return transform_batch(batch_imgs, batch_labels)
end
本質(zhì)上,當(dāng)Flux試圖檢索一批圖像時(shí),它會(huì)調(diào)用getindex(dataloader, i:i+batchsize),這在Julia中相當(dāng)于dataloader[i:i+batchsize]。
因此,我們的自定義getindex函數(shù)獲取文件名列表,獲取適當(dāng)?shù)奈募,加載這些圖像,然后將其處理并重新塑造為適當(dāng)?shù)腍EIGHT × WIDTH × COLOR × NUMBER形狀。標(biāo)簽也是如此。
然后,我們的訓(xùn)練、驗(yàn)證和測(cè)試數(shù)據(jù)加載器可以非常容易地完成:
using Flux: Data.DataLoader
using CSV
using DataFrames
using MLUtils
# dataframes containing filenames for images and corresponding labels
const train_df = DataFrame(CSV.File(dataset_dir * "train_labels.csv"))
const test_df = DataFrame(CSV.File(dataset_dir * "test_labels.csv"))
# ImageDataContainer wrappers for dataframes
# gives interface for getting the actual images and labels as tensors
const train_dataset = ImageDataContainer(train_df, train_dir)
const test_dataset = ImageDataContainer(test_df, test_dir)
# randomly sort train dataset into training and validation sets
const train_set, val_set = splitobs(train_dataset, at=0.7, shuffle=true)
const train_loader = DataLoader(train_set, batchsize=BATCH_SIZE, shuffle=true)
const val_loader = DataLoader(val_set, batchsize=BATCH_SIZE, shuffle=true)
const test_loader = DataLoader(test_dataset, batchsize=BATCH_SIZE)
制作模型
數(shù)據(jù)加載器準(zhǔn)備就緒后,下一步是創(chuàng)建模型。首先是基于ResNet的遷移學(xué)習(xí)模型。事實(shí)證明,這項(xiàng)工作相對(duì)困難。
在Metalhead.jsl包中(包含用于遷移學(xué)習(xí)的計(jì)算機(jī)視覺Flux模型),創(chuàng)建具有預(yù)訓(xùn)練權(quán)重的ResNet18模型應(yīng)該與model = ResNet(18; pretrain = true)一樣簡(jiǎn)單。
然而,至少在編寫本文時(shí),創(chuàng)建預(yù)訓(xùn)練的模型會(huì)導(dǎo)致錯(cuò)誤。這很可能是因?yàn)镸etalhead.jsl仍在添加預(yù)訓(xùn)練的權(quán)重。
我終于在HuggingFace上找到了包含權(quán)重的.tar.gz文件:
https://huggingface.co/FluxML/resnet18
我們可以使用以下代碼加載權(quán)重,并創(chuàng)建我們自己的自定義Flux模型:
using Flux
using Metalhead
using Pipe
using BSON
# load in saved params from bson
resnet = ResNet(18)
@pipe joinpath(@__DIR__, "resnet18.bson") |> BSON.load(_)[:model] |> Flux.loadmodel!(resnet, _)
# last element of resnet18 is a chain
# since we're removing the last element, we just want to recreate it, but with different number of classes
# probably a more elegant, less hard-coded way to do this, but whatever
baseline_model = Chain(
resnet.layers[1:end-1],
Chain(
AdaptiveMeanPool((1, 1)),
Flux.flatten,
Dense(512 => N_CLASSES)
)
)
(注意:如果有比這更優(yōu)雅的方法來更改ResNet的最后一層,請(qǐng)告訴我。)
創(chuàng)建了預(yù)訓(xùn)練的遷移學(xué)習(xí)模型后,這只剩下兩個(gè)孿生網(wǎng)絡(luò)模型。然而,與遷移學(xué)習(xí)不同,我們必須學(xué)習(xí)如何手動(dòng)創(chuàng)建模型。(如果你習(xí)慣了PyTorch,這就是Flux與PyTorch的不同之處。)
使用Flux文檔和其他在線資源創(chuàng)建CNN相對(duì)容易。然而,F(xiàn)lux沒有內(nèi)置層來表示具有參數(shù)共享的Twin網(wǎng)絡(luò)。它最接近的是平行層,它不使用參數(shù)共享。
然而,F(xiàn)lux在這里有關(guān)于如何創(chuàng)建自定義多個(gè)輸入或輸出層的文檔。在我們的例子中,我們可以用來創(chuàng)建自定義Twin層的代碼如下:
using Flux
"Custom Flux NN layer which will create twin network from `path` with shared parameters and combine their output with `combine`."
struct Twin{T,F}
combine::F
path::T
end
# define the forward pass of the Twin layer
# feeds both inputs, X, through the same path (i.e., shared parameters)
# and combines their outputs
Flux.@functor Twin
(m::Twin)(Xs::Tuple) = m.combine(map(X -> m.path(X), Xs)...)
首先請(qǐng)注意,它以一個(gè)簡(jiǎn)單的結(jié)構(gòu)Twin開頭,包含兩個(gè)字段:combine和path。path是我們的兩個(gè)圖像輸入將經(jīng)過的網(wǎng)絡(luò),而combine是在最后將輸出組合在一起的函數(shù)。
使用Flux.@functor告訴Flux將我們的結(jié)構(gòu)像一個(gè)常規(guī)的Flux層一樣對(duì)待。(m::Twin)(Xs::Tuple) = m.combine(map(X -> m.path(X), Xs)…)定義了前向傳遞,其中元組Xs中的所有輸入X都通過path饋送,然后所有輸出都通過combine。
要使用自定義CNN架構(gòu)創(chuàng)建Twin網(wǎng)絡(luò),我們可以執(zhí)行以下操作:
using Flux
twin_model = Twin(
# this layer combines the outputs of the twin CNNs
Flux.Bilinear((32,32) => 1),
# this is the architecture that forms the path of the twin network
Chain(
# layer 1
Conv((5,5), 3 => 18, relu),
MaxPool((3,3), stride=3),
# layer 2
Conv((5,5), 18 => 36, relu),
MaxPool((2,2), stride=2),
# layer 3
Conv((3,3), 36 => 72, relu),
MaxPool((2,2), stride=2),
Flux.flatten,
# layer 4
Dense(19 * 19 * 72 => 64, relu),
# Dropout(0.1),
# output layer
Dense(64 => 32, relu)
)
)
在本例中,我們實(shí)際上使用Flux.Biliner層作為組合,這實(shí)質(zhì)上創(chuàng)建了一個(gè)連接到兩個(gè)獨(dú)立輸入的輸出層。上面,兩個(gè)輸入是路徑的輸出,即自定義CNN架構(gòu)。或者,我們可以以某種方式使用hcat或vcat作為組合,然后在最后添加一個(gè)Dense層,但這個(gè)解決方案似乎更適合這個(gè)問題。
現(xiàn)在,要使用ResNet創(chuàng)建Twin網(wǎng)絡(luò),我們可以執(zhí)行以下操作:
using Flux
using Metalhead
using Pipe
using BSON
# load in saved params from bson
resnet = ResNet(18)
@pipe joinpath(@__DIR__, "resnet18.bson") |> BSON.load(_)[:model] |> Flux.loadmodel!(resnet, _)
# create twin resnet model
twin_resnet = Twin(
Flux.Bilinear((32,32) => 1),
Chain(
resnet.layers[1:end-1],
Chain(
AdaptiveMeanPool((1, 1)),
Flux.flatten,
Dense(512 => 32)
)
)
)
請(qǐng)注意,我們?nèi)绾问褂门c之前相同的技巧,并使用Flux.雙線性層作為組合,并使用與之前類似的技巧來使用預(yù)訓(xùn)練的ResNet作為路徑。
訓(xùn)練時(shí)間
現(xiàn)在我們的數(shù)據(jù)加載器和模型準(zhǔn)備就緒,剩下的就是訓(xùn)練了。通常,在Flux中,可以使用一個(gè)簡(jiǎn)單的一行代碼,@epochs 2 Flux.train!(loss, ps, dataset, opt),但我們確實(shí)有一些定制的事情要做。
首先,非孿生網(wǎng)絡(luò)的訓(xùn)練循環(huán):
using Flux
using Flux: Losses.logitbinarycrossentropy
using CUDA
using ProgressLogging
using Pipe
using BSON
"Stores the history through all the epochs of key training/validation performance metrics."
mutable struct TrainingMetrics
val_acc::Vector{AbstractFloat}
val_loss::Vector{AbstractFloat}
TrainingMetrics(n_epochs::Integer) = new(zeros(n_epochs), zeros(n_epochs))
end
"Trains given model for a given number of epochs and saves the model that performs best on the validation set."
function train!(model, n_epochs::Integer, filename::String)
model = model |> gpu
optimizer = ADAM()
params = Flux.params(model[end]) # transfer learning, so only training last layers
metrics = TrainingMetrics(n_epochs)
# zero init performance measures for epoch
epoch_acc = 0.0
epoch_loss = 0.0
# so we can automatically save the model with best val accuracy
best_acc = 0.0
# X and y are already in the right shape and on the gpu
# if they weren't, Zygote.jl would throw a fit because it needs to be able to differentiate this function
loss(X, y) = logitbinarycrossentropy(model(X), y)
@info "Beginning training loop..."
for epoch_idx ∈ 1:n_epochs
@info "Training epoch $(epoch_idx)..."
# train 1 epoch, record performance
@withprogress for (batch_idx, (imgs, labels)) ∈ enumerate(train_loader)
X = @pipe imgs |> gpu |> float32.(_)
y = @pipe labels |> gpu |> float32.(_)
gradients = gradient(() -> loss(X, y), params)
Flux.Optimise.update!(optimizer, params, gradients)
@logprogress batch_idx / length(enumerate(train_loader))
end
# reset variables
epoch_acc = 0.0
epoch_loss = 0.0
@info "Validating epoch $(epoch_idx)..."
# val 1 epoch, record performance
@withprogress for (batch_idx, (imgs, labels)) ∈ enumerate(val_loader)
X = @pipe imgs |> gpu |> float32.(_)
y = @pipe labels |> gpu |> float32.(_)
# feed through the model to create prediction
y? = model(X)
# calculate the loss and accuracy for this batch, add to accumulator for epoch results
batch_acc = @pipe ((((σ.(y?) .> 0.5) .* 1.0) .== y) .* 1.0) |> cpu |> reduce(+, _)
epoch_acc += batch_acc
batch_loss = logitbinarycrossentropy(y?, y)
epoch_loss += (batch_loss |> cpu)
@logprogress batch_idx / length(enumerate(val_loader))
end
# add acc and loss to lists
metrics.val_acc[epoch_idx] = epoch_acc / length(val_set)
metrics.val_loss[epoch_idx] = epoch_loss / length(val_set)
# automatically save the model every time it improves in val accuracy
if metrics.val_acc[epoch_idx] >= best_acc
@info "New best accuracy: $(metrics.val_acc[epoch_idx])! Saving model out to $(filename).bson"
BSON.@save joinpath(@__DIR__, "$(filename).bson")
best_acc = metrics.val_acc[epoch_idx]
end
end
return model, metrics
end
這里有很多要解開的東西,但本質(zhì)上這做了一些事情:
它創(chuàng)建了一個(gè)結(jié)構(gòu),用于跟蹤我們想要的任何驗(yàn)證度量。在這種情況下是每個(gè)epoch的損失和精度。
它只選擇要訓(xùn)練的最后一層參數(shù)。如果我們?cè)敢,我們可以?xùn)練整個(gè)模型,但這在計(jì)算上會(huì)更費(fèi)力。這是不必要的,因?yàn)槲覀兪褂玫氖穷A(yù)訓(xùn)練的權(quán)重。
對(duì)于每個(gè)epoch,它都會(huì)遍歷要訓(xùn)練的訓(xùn)練集的所有批次。然后,它計(jì)算整個(gè)驗(yàn)證集(當(dāng)然是成批的)的準(zhǔn)確性和損失。如果提高了epoch的驗(yàn)證精度,則可以保存模型。如果沒有,它將繼續(xù)到下一個(gè)時(shí)代。
請(qǐng)注意,我們可以在這里做更多的工作,例如,提前停止,但以上內(nèi)容足以了解大致情況。
接下來,Twin網(wǎng)絡(luò)的訓(xùn)練循環(huán)非常相似,但略有不同:
using Flux
using Flux: Losses.logitbinarycrossentropy
using CUDA
using ProgressLogging
using Pipe
using BSON
"Trains given twin model for a given number of epochs and saves the model that performs best on the validation set."
function train!(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) # if custom CNN, need to train all params
metrics = TrainingMetrics(n_epochs)
# zero init performance measures for epoch
epoch_acc = 0.0
epoch_loss = 0.0
# so we can automatically save the model with best val accuracy
best_acc = 0.0
# X and y are already in the right shape and on the gpu
# if they weren't, Zygote.jl would throw a fit because it needs to be able to differentiate this function
loss(Xs, y) = logitbinarycrossentropy(model(Xs), y)
@info "Beginning training loop..."
for epoch_idx ∈ 1:n_epochs
@info "Training epoch $(epoch_idx)..."
# train 1 epoch, record performance
@withprogress for (batch_idx, ((imgs?, labels?), (imgs?, labels?))) ∈ enumerate(zip(train_loader?, train_loader?))
X? = @pipe imgs? |> gpu |> float32.(_)
y? = @pipe labels? |> gpu |> float32.(_)
X? = @pipe imgs? |> gpu |> float32.(_)
y? = @pipe labels? |> gpu |> float32.(_)
Xs = (X?, X?)
y = ((y? .== y?) .* 1.0) # y represents if both images have the same label
gradients = gradient(() -> loss(Xs, y), params)
Flux.Optimise.update!(optimizer, params, gradients)
@logprogress batch_idx / length(enumerate(train_loader?))
end
# reset variables
epoch_acc = 0.0
epoch_loss = 0.0
@info "Validating epoch $(epoch_idx)..."
# val 1 epoch, record performance
@withprogress for (batch_idx, ((imgs?, labels?), (imgs?, labels?))) ∈ enumerate(zip(val_loader?, val_loader?))
X? = @pipe imgs? |> gpu |> float32.(_)
y? = @pipe labels? |> gpu |> float32.(_)
X? = @pipe imgs? |> gpu |> float32.(_)
y? = @pipe labels? |> gpu |> float32.(_)
Xs = (X?, X?)
y = ((y? .== y?) .* 1.0) # y represents if both images have the same label
# feed through the model to create prediction
y? = model(Xs)
# calculate the loss and accuracy for this batch, add to accumulator for epoch results
batch_acc = @pipe ((((σ.(y?) .> 0.5) .* 1.0) .== y) .* 1.0) |> cpu |> reduce(+, _)
epoch_acc += batch_acc
batch_loss = logitbinarycrossentropy(y?, y)
epoch_loss += (batch_loss |> cpu)
@logprogress batch_idx / length(enumerate(val_loader))
end
# add acc and loss to lists
metrics.val_acc[epoch_idx] = epoch_acc / length(val_set)
metrics.val_loss[epoch_idx] = epoch_loss / length(val_set)
# automatically save the model every time it improves in val accuracy
if metrics.val_acc[epoch_idx] >= best_acc
@info "New best accuracy: $(metrics.val_acc[epoch_idx])! Saving model out to $(filename).bson"
BSON.@save joinpath(@__DIR__, "$(filename).bson")
best_acc = metrics.val_acc[epoch_idx]
end
end
return model, metrics
end
首先注意,我們使用了一個(gè)同名函數(shù)train!,但具有稍微不同的函數(shù)簽名。這允許Julia根據(jù)我們正在訓(xùn)練的網(wǎng)絡(luò)類型來分配正確的功能。
還要注意,Twin ResNet模型凍結(jié)其預(yù)訓(xùn)練的參數(shù),而我們訓(xùn)練所有Twin自定義CNN參數(shù)。
除此之外,訓(xùn)練循環(huán)的其余部分基本相同,只是我們必須使用兩個(gè)訓(xùn)練數(shù)據(jù)加載器和兩個(gè)驗(yàn)證數(shù)據(jù)加載器。這些為我們提供了兩個(gè)輸入和每批兩組標(biāo)簽,我們將其適當(dāng)?shù)剌斎氲絋win模型中。
最后,請(qǐng)注意,Twin模型預(yù)測(cè)兩個(gè)輸入圖像是否具有相同的標(biāo)簽,而常規(guī)非Twin網(wǎng)絡(luò)僅直接預(yù)測(cè)標(biāo)簽。
這樣,為所有三個(gè)模型的測(cè)試集構(gòu)建測(cè)試循環(huán)應(yīng)該不會(huì)太難。因?yàn)檫@篇文章的目的是要解決我在網(wǎng)上找不到例子的主要痛點(diǎn),所以我將把測(cè)試部分作為練習(xí)留給讀者。
最后
最大的挑戰(zhàn)是縮小從相對(duì)簡(jiǎn)單的示例到更先進(jìn)的技術(shù)之間的差距,而這些技術(shù)缺乏示例。但這也揭示了Julia的優(yōu)勢(shì):因?yàn)樗旧砭秃芸,所以搜索包的源代碼以找到答案通常非常容易。
有幾次,我發(fā)現(xiàn)自己在瀏覽Flux源代碼,以了解一些東西是如何工作的。每一次我都能非常輕松快速地找到答案。我不確定我是否有勇氣為PyTorch嘗試類似的東西。
另一個(gè)挑戰(zhàn)是Metalhead.jsl的不成熟狀態(tài),這在Julia生態(tài)系統(tǒng)中肯定不是獨(dú)一無二的,因?yàn)樗墓δ懿煌暾?/p>
最后一個(gè)想法是,我發(fā)現(xiàn)Flux非常有趣和優(yōu)雅……一旦我掌握了它的竅門。我肯定會(huì)在未來與Flux一起進(jìn)行更深入的學(xué)習(xí)。
感謝閱讀!
參考引用
[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
原文標(biāo)題 : 使用Flux.jl進(jìn)行圖像分類

發(fā)表評(píng)論
請(qǐng)輸入評(píng)論內(nèi)容...
請(qǐng)輸入評(píng)論/評(píng)論長(zhǎng)度6~500個(gè)字
最新活動(dòng)更多
-
3月27日立即報(bào)名>> 【工程師系列】汽車電子技術(shù)在線大會(huì)
-
4月30日立即下載>> 【村田汽車】汽車E/E架構(gòu)革新中,新智能座艙挑戰(zhàn)的解決方案
-
5月15-17日立即預(yù)約>> 【線下巡回】2025年STM32峰會(huì)
-
即日-5.15立即報(bào)名>>> 【在線會(huì)議】安森美Hyperlux™ ID系列引領(lǐng)iToF技術(shù)革新
-
5月15日立即下載>> 【白皮書】精確和高效地表征3000V/20A功率器件應(yīng)用指南
-
5月16日立即參評(píng) >> 【評(píng)選啟動(dòng)】維科杯·OFweek 2025(第十屆)人工智能行業(yè)年度評(píng)選
推薦專題
- 1 UALink規(guī)范發(fā)布:挑戰(zhàn)英偉達(dá)AI統(tǒng)治的開始
- 2 北電數(shù)智主辦酒仙橋論壇,探索AI產(chǎn)業(yè)發(fā)展新路徑
- 3 降薪、加班、裁員三重暴擊,“AI四小龍”已折戟兩家
- 4 “AI寒武紀(jì)”爆發(fā)至今,五類新物種登上歷史舞臺(tái)
- 5 國(guó)產(chǎn)智駕迎戰(zhàn)特斯拉FSD,AI含量差幾何?
- 6 光計(jì)算迎來商業(yè)化突破,但落地仍需時(shí)間
- 7 東陽光:2024年扭虧、一季度凈利大增,液冷疊加具身智能打開成長(zhǎng)空間
- 8 地平線自動(dòng)駕駛方案解讀
- 9 封殺AI“照騙”,“淘寶們”終于不忍了?
- 10 優(yōu)必選:營(yíng)收大增主靠小件,虧損繼續(xù)又逢關(guān)稅,能否乘機(jī)器人東風(fēng)翻身?