跳至主要內容
  • Hostloc 空間訪問刷分
  • 售賣場
  • 廣告位
  • 賣站?

4563博客

全新的繁體中文 WordPress 網站
  • 首頁
  • 基于 Julia 的深度学习入门
未分類
24 5 月 2020

基于 Julia 的深度学习入门

基于 Julia 的深度学习入门

資深大佬 : tczhangzhi 1

搬运自我的知乎: https://zhuanlan.zhihu.com/p/142667683

这段时间计算机视觉领域出现了一些使用 Julia 开源的相关工作,要科学合理地对比这些相关工作,储备新的炼丹技巧,笔者不得不开始熟悉 Julia。笔者从周一拿到 Julia 文档开始,这周的试验都是使用 Julia 完成的。这里,打算先说一说笔者的几个感受,帮助大家判断一下自己是否需要着手入坑这门语言:

实用性:★★★★☆

两三年前研究运筹学的时候用 Julia 做最优化问题,感觉比 Cplex 、Matlab 好用。近两年 Julia 开源的深度学习工作逐渐增多,研究的一般是基本问题,在 toy 数据集上跑试验。近期也出现了一些 CV 领域的项目。

生态:★★★☆☆

深度学习库 Flux 和 GPU 计算库 CuArray 基本稳定下来,周边项目更新迅速,比如常用的预训练模型也都可以在 Julia 社区中找到靠谱的库了(如 MetalHead )。当然,周边项目的快速迭代也会导致一些库动不动就报错(甚至在安装时都要费一番功夫)。另外比较有特点的是,大部分常用的 Python 库都有 PyCall 封装的跟进,实在不行自己用 PyCall 、JavaCall 、Clang 写个胶水层也能用。

易用性:★★★★★

Julia 的语法真的很简单,混合了 Python 和 Matlab,30 分钟入门后续查漏补缺即可。Julia 内置了大量的科学计算方法(符号),确实比 Python 直观和好写了很多。美中不足的是社区现有的代码和官方最佳实践比较少,笔者正在试图在这方面贡献一些工作。

运行速度:★★☆☆☆

运行速度比 PyThon 稍有提高,但是第一次运行需要编译因此调试时体验稍差于 Python。多线程跑崩过系统,GPU 的分布式框架还不太完善。

一、装机必备

在开始之前推荐一些装机必备。考虑到同学们比较熟悉 Python 因此使用 Python 中的 toolbox 进行类比,懒癌患者可以直接装推荐安装的部分:

  • Julia 对应 Python
  • Pkg3 对应 pip3
  • JuliaPro 对应 Anaconda (推荐直接安装这个)
  • IJulia + Jupyter 对应 IPython + Jupyter (推荐使用)
  • VSCode Julia 插件 对应 VSCode Python 插件
  • PkgMirrors + 浙大源 对应 清华源(推荐使用)

二、炼丹示例

Julia 的语言 Feature 较多,但都比较通俗。因此笔者比较推荐同学们在使用过程中慢慢熟悉(就算你想先慢慢学一个月再去做实验老板也不同意是吧)。如果你实在想先浏览一下基础语法,笔者总结了一个 Notebook,帮助你在 15 分钟内看完并有一个大概印象。

下面笔者总结了 Julia 版的常用 Pipeline,可以帮助同学们理解如何像用 Python + PyTorch 一样简单地使用 Julia 完成深度学习项目。在做实验的时候同学们可以简单复制粘贴,修修改改先跑上。(逃

1. MLP + MNIST 实现一个最小用例

首先,我们先完成一个最小用例,实现在 GPU 上训练一个多层感知器拟合 MNIST,了解基本操作。由于篇幅限制,完整代码请参考并运行 MLP+MNIST。

Flux 是 Julia 中的深度学习库,其完全由 Julia 实现,结构轻量化,是 Julia 中的 PyTorch 。因此首先导入 Flux 备用模型定义和反向传播(训练)。

# 从 Flux 中引入所需组件 using Flux, Flux.Data.MNIST, Statistics using Flux: onehotbatch, onecold, crossentropy, throttle, params 

尽管 Flux 中目前已经实现了 gpu() 方法,但功能有限。所幸 Flux 在 GPU 上的功能基于 CuArrays 实现,可以使用 CUDAapi, CUDAdrv, CUDAnative 来设置 Flux 使用哪个 GPU,或是只使用 CPU 。

using CUDAapi, CUDAdrv, CUDAnative gpu_id = 1  ## set < 0 for no cuda, >= 0 for using a specific device (if available)  if has_cuda_gpu() && gpu_id >=0     device!(gpu_id)     device = Flux.gpu     @info "Training on GPU-$(gpu_id)" else     device = Flux.cpu     @info "Training on CPU" end 

另外,Flux 目前仍不支持分布式 GPU 训练,要想实现该功能也需要利用上述库写 scatter 和 gather 手动实现。

与 PyTorch 相同,Flux 定义了一个开箱即用的数据集 MNIST 。这里我们调用 MNIST.images() 和 MNIST.labels() 加载数据集和对应的 label,并使用 Flux 中提供的 onehotbatch 对 label 进行 onehot 编码。

imgs = MNIST.images() labels = onehotbatch(MNIST.labels(), 0:9) 

目前,Flux 没有提供数据集切分的函数,因此我们需要手动进行该过程。具体而言,我们使用 partition 对加载进来的数据集进行切分,将每 1000 张图像分为一个 batch,并使用 |> device (遍历每个元素分别执行上文中定义的 device())全部图像迁移到 GPU 中。

train = [(cat(float.(imgs[i])..., dims = 4), labels[:,i])          for i in partition(1:60_000, 1000)] |> device 

同样,我们选择数据集中前 1000 张图片作为测试数据集,也迁移到 GPU 中。

test_X = cat(float.(MNIST.images(:test)[1:1000])..., dims = 4) |> device test_y = onehotbatch(MNIST.labels(:test)[1:1000], 0:9) |> device 

Flux 中的模型定义与 PyTorch 相似,Chain 取代了 nn.Sequential,Conv/MaxPool/Dense 等 layer 也已经封装好(封装的 cuDNN )可以直接调用。如下所示,定义模型、损失函数和评估方法只需要三段代码。

model = Chain(   Conv((2,2), 1=>16, relu),   MaxPool((2, 2)),   Conv((2,2), 16=>8, relu),   MaxPool((2, 2)),   x -> reshape(x, :, size(x, 4)),   Dense(288, 10), softmax ) |> device   loss(x, y) = crossentropy(model(x), y) accuracy(x, y) = mean(onecold(model(x)) .== onecold(y)) 

Flux 为使用者提供了 Adam 优化器,相比于 PyTorch 的版本,该 Adam 优化器似乎对学习旅更为敏感。如果遇到不收敛的情况可以尝试降低 LR 。后续打算对其 FLux 和 PyTorch 的优化器。和 PyTorch 相似,我们直接使用 ADAM(LR),定义优化器,使用 train!() 进行训练。

opt = ADAM(0.01) evalcb() = @show(accuracy(test_X, test_y))  epochs = 5  for i = 1:epochs     Flux.train!(loss, Flux.params(model), train, opt) end 

值得注意的是 Flux 中构建的图也为动态图,无需考虑计算图的构建,直接定义所需的计算操作就可以了。

进行推断时也如同 Pytorch,可以直接调用模型。如下,从测试集中选择一张图片放入模型,预测所属类别。

using Colors, FileIO, ImageShow  img = test_X[:, :, 1:1, 7:7]  println("Predicted: ", Flux.onecold(model(img |> device)) .- 1) save("outputs.jpg", collect(test_X[:, :, 1, 7])) 

2. VGG + Cifar 封装常用方法 Finetune 模型

在试验和竞赛中,我们通常要对读入图像进行增广;模型也通常是基于某个 pretrained 的模型 Finetune 的,因此接下来我们看如何对这些内容进行封装。由于篇幅限制,这里只说明重要部分,完整代码请参考并运行 VGG+Cifar10。

目前 Flex 和周边的生态还不太完善,图像增强部分的实现实属有限。这里我们参照 pytorch 实现最基本的图像增广的预处理过程。更为丰富的预处理恐怕只能自己编写或是等待官方更新,当然,这也是重新造轮子的好机会~

function resize_smallest_dimension(im, len)   reduction_factor = len/minimum(size(im)[1:2])   new_size = size(im)   new_size = (       round(Int, size(im,1)*reduction_factor),       round(Int, size(im,2)*reduction_factor),   )   if reduction_factor < 1.0     # Images.jl's imresize() needs to first lowpass the image, it won't do it for us     im = imfilter(im, KernelFactors.gaussian(0.75/reduction_factor), Inner())   end   return imresize(im, new_size) end  # Take the len-by-len square of pixels at the center of image `im` function center_crop(im, len)   l2 = div(len,2)   adjust = len % 2 == 0 ? 1 : 0   return im[div(end,2)-l2:div(end,2)+l2-adjust,div(end,2)-l2:div(end,2)+l2-adjust] end  function preprocess(im)   # Resize such that smallest edge is 256 pixels long   im = resize_smallest_dimension(im, 256)    # Center-crop to 224x224   im = center_crop(im, 224)    # Convert to channel view and normalize (these coefficients taken   # from PyTorch's ImageNet normalization code)   μ = [0.485, 0.456, 0.406]   # the sigma numbers are suspect: they cause the image to go outside of 0..1   # 1/0.225 = 4.4 effective scale   σ = [0.229, 0.224, 0.225]   #im = (channelview(im) .- μ)./σ   im = (channelview(im) .- μ)    # Convert from CHW (Image.jl's channel ordering) to WHCN (Flux.jl's ordering)   # and enforce Float32, as that seems important to Flux   # result is (224, 224, 3, 1)   #return Float32.(permutedims(im, (3, 2, 1))[:,:,:,:].*255)  # why   return Float32.(permutedims(im, (3, 2, 1))[:,:,:,:]) end 

这里将 MNIST 的数据集切分方法进行封装,使用 get_processed_data 和 get_test_data 构建训练集合、验证集合和测试集合。

using Metalhead: trainimgs using Images, ImageMagick  function get_processed_data(args)     # Fetching the train and validation data and getting them into proper shape      X = trainimgs(CIFAR10)     imgs = [preprocess(X[i].img) for i in 1:40000]     #onehot encode labels of batch         labels = onehotbatch([X[i].ground_truth.class for i in 1:40000],1:10)      train_pop = Int((1-args.splitr_)* 40000)     train = device.([(cat(imgs[i]..., dims = 4), labels[:,i]) for i in partition(1:train_pop, args.batchsize)])     valset = collect(train_pop+1:40000)     valX = cat(imgs[valset]..., dims = 4) |> device     valY = labels[:, valset] |> device      val = (valX,valY)     return train, val end  function get_test_data()     # Fetch the test data from Metalhead and get it into proper shape.     test = valimgs(CIFAR10)      # CIFAR-10 does not specify a validation set so valimgs fetch the testdata instead of testimgs     testimgs = [preprocess(test[i].img) for i in 1:1000]     testY = onehotbatch([test[i].ground_truth.class for i in 1:1000], 1:10) |> device     testX = cat(testimgs..., dims = 4) |> device      test = (testX,testY)     return test end 

Julia 中预训练模型库正蓬勃发展,比较成熟的有 Metalhead (类似于 Torchvision )等。这里我们使用 Metalhead 中提供的模型结构和预训练参数构建 VGG19,并替换后面的层完成当前任务。值得一提的是,目前 EfficientNet 还没有较为优雅的 Julia 封装,实属一大遗憾。

using Metalhead  vgg = VGG19() model = Chain(vgg.layers[1:end-6],               Dense(512, 4096, relu),               Dropout(0.5),               Dense(4096, 4096, relu),               Dropout(0.5),               Dense(4096, 10)) |> device Flux.trainmode!(model, true) 

为了方便试验和记录,我们参照官方实现封装超参数和训练过程。在训练过程中,我们可以定义一个回调函数打印验证集的损失函数:throttle(() -> @show(loss(val…)), args.throttle)。

using Parameters: @with_kw @with_kw mutable struct Args     batchsize::Int = 128     throttle::Int = 10     lr::Float64 = 5e-5     epochs::Int = 10     splitr_::Float64 = 0.1 end  function train(model; kws...)     # Initialize the hyperparameters     args = Args(; kws...)          # Load the train, validation data      train, val = get_processed_data(args)      @info("Constructing Model")     # Defining the loss and accuracy functions      loss(x, y) = logitcrossentropy(model(x), y)      ## Training     # Defining the callback and the optimizer     evalcb = throttle(() -> @show(loss(val...)), args.throttle)     opt = ADAM(args.lr)     @info("Training....")     # Starting to train models     [email protected] args.epochs Flux.train!(loss, params(model), train, opt, cb=evalcb) end 

3. ResNet + ImageNet 大型数据集上的标准训练过程

在学会在中小型数据集上完成试验后,我们往往要将试验迁移到大型数据集上。训练过程也会增加很多读取、存储、日志等内容。由于篇幅限制,这里只说明重要部分,完整代码请参考并运行 ResNet+ImageNet。

不同于 PyTorch,目前 Flux 对 Dataset 和 Dataloader 的支持十分有限。官方目前正着力于添加相关功能,不久后可能有相关实现。这里我们模仿 PyTorch 多线程读取数据集并生成 Dataloader 。

struct ImagenetDataset     # Data we're initialized with     dataset_root::String     batch_size::Int     data_loader::Function      # Data we calculate once, at startup     filenames::Vector{String}     queue_pool::QueuePool      function ImagenetDataset(dataset_root::String, num_workers::Int, batch_size::Int,                              data_loader::Function = imagenet_val_data_loader)         # Scan dataset_root for files         filenames = filter(f -> endswith(f, ".JPEG"), recursive_readdir(dataset_root))          @assert !isempty(filenames) "Empty dataset folder!"         @assert num_workers >= 1 "Must have nonnegative integer number of workers!"         @assert batch_size >= 1 "Must have nonnegative integer batch size!"          # Start our worker pool         @info("Adding $(num_workers) new data workers...")         queue_pool = QueuePool(num_workers, data_loader, quote             # The workers need to be able to load images and preprocess them via Metalhead             using Flux, Images, Metalhead             include($(@__FILE__))         end)          return new(dataset_root, batch_size, data_loader, filenames, queue_pool)     end end  # Serialize the arguments needed to recreate this ImagenetDataset function freeze_args(id::ImagenetDataset)     return (id.dataset_root, length(id.queue_pool.workers), id.batch_size, id.data_loader) end Base.length(id::ImagenetDataset) = div(length(id.filenames),id.batch_size)  mutable struct ImagenetIteratorState     batch_idx::Int     job_offset::Int          function ImagenetIteratorState(id::ImagenetDataset)         @info("Creating IIS with $(length(id.filenames)) images")          # Build permutation for this iteration         permutation = shuffle(1:length(id.filenames))          # Push first job, save value to get job_offset (we know that all jobs         # within this iteration will be consequtive, so we only save the offset         # of the first one, and can use that to determine the job ids of every         # subsequent job:         filename = joinpath(id.dataset_root, id.filenames[permutation[1]])         job_offset = push_job!(id.queue_pool, filename)          # Next, push every other job         for pidx in permutation[2:end]             filename = joinpath(id.dataset_root, id.filenames[pidx])             push_job!(id.queue_pool, filename)         end         return new(             0,             job_offset,         )     end end  function Base.iterate(id::ImagenetDataset, state=ImagenetIteratorState(id))     # If we're at the end of this epoch, give up the ghost     if state.batch_idx > length(id)         return nothing     end      # Otherwise, wait for the next batch worth of jobs to finish on our queue pool     next_batch_job_ids = state.job_offset .+ (0:(id.batch_size-1)) .+ id.batch_size*state.batch_idx     # Next, wait for the currently-being-worked-on batch to be done.     pairs = fetch_result.(Ref(id.queue_pool), next_batch_job_ids)     state.batch_idx += 1      # Collate X's and Y's into big tensors:     X = cat((p[1] for p in pairs)...; dims=ndims(pairs[1][1]))     Y = cat((p[2] for p in pairs)...; dims=ndims(pairs[1][2]))      # Return the fruit of our labor     return (X, Y), state end 

Julia 使用 BSON 实现模型的持久化和读取,速度令人满意。对模型保存和读取进行封装的相关实现如下:

using BSON using Tracker using Statistics, Printf using Flux.Optimise  function save_model(model, filename)     model_state = Dict(         :weights => Tracker.data.(params(model))     )     open(filename, "w") do io         BSON.bson(io, model_state)     end end  function load_model!(model, filename)     weights = BSON.load(filename)[:weights]     Flux.loadparams!(model, weights)     return model end 

4. DCGAN+Fashion/GCN+Cora 其他网络结构与数据集

近年来 GAN 和 GCN 方兴未艾,只实用 Julia 完成图像分类任务还远远不够。因此笔者正尽可能复现多种类的网络结构和任务。以 GAN 和 GCN 为例,Julia 已经能很好地完成试验目标了。由于篇幅限制,这里只说明重要部分,完整代码请参考并运行 DCGAN+Fashion 和 GCN+Cora。

与 CNN 相同,使用 Flux 可以轻松实现对 DCGAN 的定义。

function Discriminator()     return Chain(             Conv((4, 4), 1 => 64; stride = 2, pad = 1),             x->leakyrelu.(x, 0.2f0),             Dropout(0.25),             Conv((4, 4), 64 => 128; stride = 2, pad = 1),             x->leakyrelu.(x, 0.2f0),             Dropout(0.25),              x->reshape(x, 7 * 7 * 128, :),             Dense(7 * 7 * 128, 1)) end  function Generator(latent_dim)     return Chain(             Dense(latent_dim, 7 * 7 * 256),             BatchNorm(7 * 7 * 256, relu),             x->reshape(x, 7, 7, 256, :),             ConvTranspose((5, 5), 256 => 128; stride = 1, pad = 2),             BatchNorm(128, relu),             ConvTranspose((4, 4), 128 => 64; stride = 2, pad = 1),             BatchNorm(64, relu),             ConvTranspose((4, 4), 64 => 1, tanh; stride = 2, pad = 1),             ) end 

遵循动态图的反向更新策略,我们只需要像 PyTorch 一样定义对抗损失和对抗训练过程,也较为简单。

function discriminator_loss(real_output, fake_output)     real_loss = mean(logitbinarycrossentropy.(real_output, 1f0))     fake_loss = mean(logitbinarycrossentropy.(fake_output, 0f0))     return real_loss + fake_loss end  generator_loss(fake_output) = mean(logitbinarycrossentropy.(fake_output, 1f0))  function train_discriminator!(gen, dscr, x, opt_dscr, args)     noise = randn!(similar(x, (args.latent_dim, args.batch_size)))      fake_input = gen(noise)     ps = Flux.params(dscr)     # Taking gradient     loss, back = Flux.pullback(ps) do         discriminator_loss(dscr(x), dscr(fake_input))     end     grad = back(1f0)     update!(opt_dscr, ps, grad)     return loss end  function train_generator!(gen, dscr, x, opt_gen, args)     noise = randn!(similar(x, (args.latent_dim, args.batch_size)))      ps = Flux.params(gen)     # Taking gradient     loss, back = Flux.pullback(ps) do         generator_loss(dscr(gen(noise)))     end     grad = back(1f0)     update!(opt_gen, ps, grad)     return loss end  for ep in 1:args.epochs     @info "Epoch $ep"     for x in data         loss_dscr = train_discriminator!(g_model, d_model, x, opt_dscr, args)         loss_gen = train_generator!(g_model, d_model, x, opt_gen, args)     end     train_steps += 1 end 

对于其他较为复杂的 CNN 模型,例如 UNet,用户也可以自定义模块的调用过程(类似于 PyTorch 中的 forward ):

function UNet()     conv_block = (block1(1, 32), block2(32, 32*2), block2(32*2, 32*4), block2(32*4, 32*8))     conv_block2 = (block1(32*16, 32*8), block1(32*8, 32*4), block1(32*4, 32*2), block1(32*2, 32))     bottle = block2(32*8, 32*16)     upconv_block = (upconv(32*16, 32*8), upconv(32*8, 32*4), upconv(32*4, 32*2), upconv(32*2, 32))     conv_ = conv(32, 1)     UNet(conv_block, conv_block2, bottle, upconv_block, conv_) end  function (u::UNet)(x)     enc1 = u.conv_block[1](x)     enc2 = u.conv_block[2](enc1)     enc3 = u.conv_block[3](enc2)     enc4 = u.conv_block[4](enc3)          bn = u.bottle(enc4)          dec4 = u.upconv_block[1](bn)     dec4 = cat(dims=3, dec4, enc4)     dec4 = u.conv_block2[1](dec4)     dec3 = u.upconv_block[2](dec4)     dec3 = cat(dims=3, dec3, enc3)     dec3 = u.conv_block2[2](dec3)     dec2 = u.upconv_block[3](dec3)     dec2 = cat(dims=3, dec2, enc2)     dec2 = u.conv_block2[3](dec2)     dec1 = u.upconv_block[4](dec2)     dec1 = cat(dims=3, dec1, enc1)     dec1 = u.conv_block2[4](dec1)     dec1 = u.conv_(dec1) end  model = UNet() 

在 GNN 模型方面,目前较为流行的 GNN 库是 GeometricFlux,但是由于刚刚开源不久,数据读取方面的支持有限。实现应当是参考了 DGL,较为优雅且易于扩展。笔者目前也正在试图基于 LightGraphs 开发一个 GNN 库,主要着力于图的构建和分布式训练部分。

using GeometricFlux  model = Chain(GCNConv(adj_mat, num_features=>hidden, relu),               Dropout(0.5),               GCNConv(adj_mat, hidden=>target_catg),               softmax) |> gpu 

三、后记

上述示例代码和讲解均来源于笔者的开源项目 Julia-Deeplearning,目前已有的最佳实践包括:

  • Julia 教程

    • 基础语法
  • 卷积神经网络

    • MLP+MNIST
    • VGG+Cifar10
    • ResNet+ImageNet
    • UNet+ISBI
  • 生成对抗网络

    • DCGAN+Fashion
  • 图卷积网络

    • GCN+Cora

由于笔者近期试验较多,因此只能在试验之余偶尔更新。如果同学们有相关工作欢迎 PR 和提 Issue,衷心希望能够抛砖引玉对大家有所帮助~

大佬有話說 (0)

文章導覽

上一篇文章
下一篇文章

AD

其他操作

  • 登入
  • 訂閱網站內容的資訊提供
  • 訂閱留言的資訊提供
  • WordPress.org 台灣繁體中文

51la

4563博客

全新的繁體中文 WordPress 網站
返回頂端
本站採用 WordPress 建置 | 佈景主題採用 GretaThemes 所設計的 Memory
4563博客
  • Hostloc 空間訪問刷分
  • 售賣場
  • 廣告位
  • 賣站?
在這裡新增小工具