Документация Engee

Вывод формы

Flux содержит инструменты, которые помогают автоматически генерировать модели, определяя размер массивов, которые будут получать слои, без выполнения каких-либо вычислений. Это особенно полезно для сверточных моделей, где один и тот же слой Conv принимает любой размер изображения, но следующий слой может этого не делать.

Инструментом более высокого уровня является макрос @autosize, который работает с кодом, определяющим слои, и заменяет каждое вхождение _ соответствующим размером. В этом простом примере возвращается модель с Dense(845 => 10) в качестве последнего слоя:

@autosize (28, 28, 1, 32) Chain(Conv((3, 3), _ => 5, relu, stride=2), Flux.flatten, Dense(_ => 10))

Входной размер может быть указан во время выполнения, например @autosize (sz..., 1, 32) Chain(Conv(…​, но все конструкторы слоя, содержащие _, должны быть четко прописаны — макрос видит код в том виде, в котором он написан.

Этот макрос применяет функцию более низкого уровня outputsize, которую также можно использовать напрямую:

c = Conv((3, 3), 1 => 5, relu, stride=2)
Flux.outputsize(c, (28, 28, 1, 32))  # возвращает (13, 13, 5, 32)

Функция outputsize работает, передавая модели фиктивный массив, распространение которого требует минимальных ресурсов. Она должна работать для всех слоев, включая пользовательские, без дополнительной подготовки.

Ниже приведен пример автоматизации построения модели.

"""
    make_model(width, height, [inchannels, nclasses; layer_config])

Create a CNN for a given set of configuration parameters. Arguments:
- `width`, `height`: the input image size in pixels
- `inchannels`: the number of channels in the input image, default `1`
- `nclasses`: the number of output classes, default `10`
- Keyword `layer_config`: a vector of the number of channels per layer, default `[16, 16, 32, 64]`
"""
function make_model(width, height, inchannels = 1, nclasses = 10;
                    layer_config = [16, 16, 32, 64])
  # создание вектора слоев:
  conv_layers = []
  push!(conv_layers, Conv((5, 5), inchannels => layer_config[1], relu, pad=SamePad()))
  for (inch, outch) in zip(layer_config, layer_config[2:end])
    push!(conv_layers, Conv((3, 3), inch => outch, sigmoid, stride=2))
  end

  # вычисление выходных размеров после этих слоев свертки:
  conv_outsize = Flux.outputsize(conv_layers, (width, height, inchannels); padbatch=true)

  # используйте для определения соответствующего слоя Dense
  last_layer = Dense(prod(conv_outsize) => nclasses)
  return Chain(conv_layers..., Flux.flatten, last_layer)
end

m = make_model(28, 28, 3, layer_config = [9, 17, 33, 65])

Flux.outputsize(m, (28, 28, 3, 42)) == (10, 42) == size(m(randn(Float32, 28, 28, 3, 42)))

# вывод

true

Или же при использовании макроса определение make_model может заканчиваться следующим:

  # вычисление выходных измерений и построение соответствующего слоя Dense:
  return @autosize (width, height, inchannels, 1) Chain(conv_layers..., Flux.flatten, Dense(_ => nclasses))
end

Вывод списка

@autosize (size...,) Chain(Layer(_ => 2), Layer(_), ...)

Возвращает указанную модель, в которой каждый _ заменен выведенным числом, для входных данных заданного размера (size).

Неизвестные размеры обычно являются предпоследним измерением входных данных этого слоя, которое Flux рассматривает как измерение канала. (Однако несколько слоев — Dense и LayerNorm — всегда используют первое измерение.) Символ подчеркивания может появляться в качестве аргумента слоя или внутри => Его можно использовать в дальнейших вычислениях, например Dense(_ => _÷4).

Примеры

julia> @autosize (3, 1) Chain(Dense(_ => 2, sigmoid), BatchNorm(_, affine=false))
Chain(
  Dense(3 => 2, σ),                     # 8 параметров
  BatchNorm(2, affine=false),
)

julia> img = [28, 28];

julia> @autosize (img..., 1, 32) Chain(              # размер требуется только во время выполнения
          Chain(c = Conv((3,3), _ => 5; stride=2, pad=SamePad()),
                p = MeanPool((3,3)),
                b = BatchNorm(_),
                f = Flux.flatten),
          Dense(_ => _÷4, relu, init=Flux.rand32),   # можно вычислить выходной размер _÷4
          SkipConnection(Dense(_ => _, relu), +),
          Dense(_ => 10),
       )
Chain(
  Chain(
    c = Conv((3, 3), 1 => 5, pad=1, stride=2),  # 50 параметров
    p = MeanPool((3, 3)),
    b = BatchNorm(5),                   # 10 параметров плюс 10
    f = Flux.flatten,
  ),
  Dense(80 => 20, relu),                # 1_620 параметров
  SkipConnection(
    Dense(20 => 20, relu),              # 420 параметров
    +,
  ),
  Dense(20 => 10),                      # 210 параметров
)         # Всего: 10 обучаемых массивов, 2_310 параметров
          # плюс 2 необучаемых, 10 параметров, суммарный размер 10 469 КиБ.

julia> outputsize(ans, (28, 28, 1, 32))
(10, 32)

Ограничения

  • @autosize (5, 32) Flux.Bilinear(_ => 7) работает нормально, но что-то вроде Bilinear((_, _) => 7) завершится сбоем.

  • Scale() и LayerNorm() работают нормально (и используют первое измерение), Scale(,) и LayerNorm(,) завершатся сбоем, если size(x,1) != size(x,2).

outputsize(m, x_size, y_size, ...; padbatch=false)

Для модели или слоя m, принимающего в качестве входных данных несколько массивов, возвращает size(m((x, y, ...))) при size_x = size(x) и т. д.

Примеры

julia> x, y = rand(Float32, 5, 64), rand(Float32, 7, 64);

julia> par = Parallel(vcat, Dense(5 => 9), Dense(7 => 11));

julia> Flux.outputsize(par, (5, 64), (7, 64))
(20, 64)

julia> m = Chain(par, Dense(20 => 13), softmax);

julia> Flux.outputsize(m, (5,), (7,); padbatch=true)
(13, 1)

julia> par(x, y) == par((x, y)) == Chain(par, identity)((x, y))
true

Обратите внимание, что Chain принимает несколько массивов только в виде кортежа, тогда как Parallel также принимает их в виде нескольких аргументов. outputsize всегда задает кортеж.