Engee 文档
Notebook

使用多层神经网络进行分类

在这个例子中,我们将训练一个分类任务的神经网络,并将其放置在一个块中。 Engee Function,这将使我们能够轻松地将训练好的算法从一个模型转移到另一个模型。

神经网络训练

在这个问题中,我们将为标准异或问题的数据创建一个分类算法。 创建向量 noisy 用一组输入数据 x1x2,从0到1分布,并且向量 truth -我们期望从神经网络(操作)预测的结果 xor( (x1>0.5, x2>0.5)).

让我们在一个单元格中完成所有工作,然后将训练好的神经网络转移到画布上,并对代码进行所有解释。

In [ ]:
Pkg.add(["Statistics", "Flux", "Symbolics"])
   Resolving package versions...
  No Changes to `~/.project/Project.toml`
  No Changes to `~/.project/Manifest.toml`
In [ ]:
# Pkg.add( "ChainPlots" ) # Использовать осторожно, от версии к версии бывают нестыковки
In [ ]:
using Flux, Statistics, Random
Random.seed!( 2 )           # Обеспечим управляемость процесса обучения

# Архитектура модели: два полносвязанных слоя с небольшим количеством нейронов в каждом.
model = Chain(
      Dense(2 => 3, tanh),
      Dense(3 => 2),        # Сколько классов, столько и выходов нейросети
      softmax )

# Генерация входных данных
inputs = rand( Float32, 2, 1000 );                                   # 2×1000 Matrix{Float32}
truth = [ xor(col[1]>0.5, col[2]>0.5) for col in eachcol(inputs) ];  # Vector{Bool} из 1000 элементов

# Сохраним на будущее прогноз "необученной" модели
probs1 = model( inputs );

# Подготовка данных и обучение
targets = Flux.onehotbatch( truth, [true, false] );          # Раскладываем выходную переменную на логиты и создаем загрузчик данных
data = Flux.DataLoader( (inputs, targets), batchsize=64, shuffle=true );
opt_state = Flux.setup( Adam( 0.01 ), model );               # Настройки процедуры оптимизации и конкретная функция потерь
loss(, y) = Flux.crossentropy( , y )
accuracy(, y) = mean( Flux.onecold(  ) .== Flux.onecold( y ))
loss_history, accuracy_history = [], []                      # Производим обучение, записывая результаты
for i in 1:5000
    Flux.train!( model, data, opt_state) do m, x, y
        loss( m(x), y ) # Функция потерь - ошибка на каждом элементе датасета
    end
    push!( loss_history, loss( model(inputs), targets ) ) # Запомним значение функции потерь и точности прогноза
    push!( accuracy_history, accuracy( model(inputs), targets ) )
end

# Прогноз модели после обучения
probs2 = model( inputs );

# Выведем график, по которому можно оценить качество обучения
gr()
plot( [ loss_history, accuracy_history], size=(300,200), label=["loss" "accuracy"], leg=:right )
Out[0]:

学习成果

我们专门保存了模型在训练前后的预测:

In [ ]:
println( "Точность прогноза перед обучением: ", 100 * mean( (probs1[1,:] .> 0.5) .== truth ), "%" )
println( "Точность прогноза после обучения: ", 100 * mean( (probs2[1,:] .> 0.5) .== truth ), "%" )
Точность прогноза перед обучением: 50.4%
Точность прогноза после обучения: 97.1%
In [ ]:
# Выведем график исходных данных
p_true = scatter( inputs[1,:], inputs[2,:], zcolor=truth, title="Исходные данные" );
p_raw = scatter( inputs[1,:], inputs[2,:], zcolor=probs1[1,:], title="Прогноз до обучения" );
p_done = scatter( inputs[1,:], inputs[2,:], zcolor=probs2[1,:], title="После обучения" );

plot(p_true, p_raw, p_done, layout=(1,3), size=(700,200), titlefont=font(9), ms=3.5, legend=false, cbar=false )
Out[0]:

将神经网络传输到Engee功能块

我们将通过将符号变量替换为其输入并获得符号表达式而不是输出来为该神经网络生成Julia代码。

让我们把它放在街区里 Engee Function 以获得可以复制并粘贴到任何其他模型中的块。

In [ ]:
# Сгенерируем новое изображение для размещения на лицевой стороне блока
# (успех зависит от стабильности текущей версии ChainPlots)

# using ChainPlots
# p = plot( model,
#     titlefontsize=10, size=(300,300),
#     xticks=:none, series_annotations="", markersize=8,
#     markercolor="white", markerstrokewidth=4, linewidth=1 )
# savefig( p, "$(@__DIR__)/neural_net_block_mask.png");
In [ ]:
# Создадим код нейросети

using Symbolics
@variables x1 x2
s = model( [x1, x2] );

# Загрузим модель, если она еще не открыта на холсте
if "neural_classification"  getfield.(engee.get_all_models(), :name)
    engee.load( "$(@__DIR__)/neural_classification.engee");
end

# Шаблон кода, который мы поместим в блок Engee Function
code_strings = """
struct Block <: AbstractCausalComponent; end

# У нейросети два выхода: s[1] и s[2]
nn(x1, x2) = ($(s[1]), $(s[2]))

# Вычислим выходы нейросети и вернем результат классификации: 0 или 1
function (c::Block)(t::Real, x1, x2)
    # "Вероятность" каждого из классов
    c1, c2 = nn(x1, x2)
    # Вычисляем выходное значение по результатам классификации
    # - если больше вероятность c1, то будет выбран класс true (1)
    # - если больше – вероятность c2, то будет выбран класс false (2)
    return (c1 > c2) ? 1 : 0
end
"""

# Какой блок модели должен содержать код нейросети?
block_address = "neural_classification/Engee Function"
engee.set_param!( block_address, "StepMethodCode" => code_strings)

# Сохраним модель после изменения 
engee.save( "neural_classification", "$(@__DIR__)/neural_classification.engee"; force = true )
Out[0]:
Model(
	name: neural_classification
	id: 995f8eb6-3102-488a-8dac-5040e7bd1526
)

所需块的地址可以在该块的设置中复制,从信息面板上的路径到模型字段。

image.png

让我们运行模型并查看结果。:

In [ ]:
model_data = engee.run( "neural_classification" );

# Подготовим выходные переменные
model_x1 = model_data["X1"].value;
model_x2 = model_data["X2"].value;
model_y = vec( hcat( model_data["Y"].value... ));

# Построим график
scatter( model_x1, model_x2, model_y, ms=2.5, msw=.5, leg=false, zcolor=model_y, c=:viridis,
         xlimits=(0,1), ylimits=(0,1), title="Прогноз от блока Engee Function", titlefont=font(10) )
Out[0]:

守则的解释

In [ ]:
# plot( model, size=(600, 350) )

让我们来看看分类的神经网络学习过程的几个特征,即:

*softmax功能,
*one-hot编码,
*创建数据加载器,
*损失函数"交叉熵",
*预测精度的计算。

模型=链(
      密集(2=>3,tanh),
      密集(3=>2),
      softmax)

首先,你可以看到我们神经网络的任务是确定一个对象应该属于哪个类。

我们的神经网络不会将两个输入参数转换为一个输出变量。 输出变量的数量等于类的数量。

请注意:最后一个FC层具有线性激活。 在它之后有一些**"softmax层"**。 Softmax是对数字的操作,有时表现为激活。 但在包 Flux 否则就接受了。 它的本质是什么? softmax?

Softmax将神经网络的输出值转换为概率。 这些是二进制交叉熵损失函数的稍微更正确的输入(见下文)。

假设一个神经网络有 N 输出在输出层,之后是函数 softmax. 它需要每个输入值。 x_i,将其指数提升为幂 ()和每个值 计算输出 . 输出函数将每个 对于所有的总和 在输出中,我们得到logits–严格的正数,其总和为1。

目标=通量。onehotbatch(真理,[真,假])

我们的分类器任务被组织起来,以便网络返回 [1, 0],或 [0, 1]. 这是为什么?

想象一下,一个神经网络应该返回你一个类号,十有八九。 如果神经网络错误并且返回2而不是1,则MSE返回error(2-1)=1。 如果神经网络是错误的并且返回10而不是1,则MSE返回error(10-1)=9,尽管此错误通常不是比所有其他错误更严重的错误。 我们需要建立在别的东西上。 编码一个热允许您避免比较类号和比较网络"置信度"在特定类中的分布。

但在训练样本中,输出变量的值仍然是标量的。: truefalse. 功能 onehot 将它们转换为两个值的向量: true[1,0],而 false 在向量上 [0,1].

数据=通量。DataLoader((嘈杂,目标))

DataLoader -将数据馈送到神经网络的稍微更优雅的方法,该方法在将参数向量馈送到网络之前不需要转置参数向量。 您也可以将参数传递给它。 shuffle = true 使样本在训练的每个epoch混合,以及 batchsize=64 以并行执行。 这就是这个对象输入神经网络的元素的样子。:

In [ ]:
data = Flux.DataLoader( (inputs, targets), batchsize=1 );
first( data )
Out[0]:
(Float32[0.5859486; 0.54989403;;], Bool[0; 1;;])

正如我们所看到的,对象的第一个元素 DataLoader 它由两部分组成:

-特征向量-馈送到神经网络输入的标量值,
-预测向量-所需的类,按类型编码 one-hot.

损失(ζ,y)=通量。跨界(ỹ,y)

如上所述,有一条规则不能在分类任务中使用均方误差(MSE)。 神经网络的学习速度非常慢,特别是如果分类是多类的。 我们做什么回报?

如果分类任务按照我们的示例进行组织,那么交叉熵通常用作损失函数(crossentropy),或其二进制版本(binarycrossentropy)如果只有两个类。 有时一个操作被排除在神经网络之外。 softmax 为了加快其操作,那么您可以通过指定在损失函数中执行 logitcrossentropylogitbinarycrossentropy.

精度(ψ,y)=均值(通量。onecold(ỹ)。==通量。(y))

我们保持预测的准确性(准确性)-正确猜测值的百分比。 功能 onecold 相对于执行反向操作 onehot. 运作 onehot-编码找到向量中最大的元素,并用输出向量中的数字1表示它,其中所有其他位置都是0。 反过来,操作 onecold 查找输入向量中最大的元素,并在输出端输出单个值-与此元素对应的类标签(如果未指定标签,则为序数)。

这个函数可以更容易地定义,但高级函数通常允许您避免大量错误或至少获得更有价值的错误消息。

不是每次发射都会导致一个好的结果,所以在这个例子的开始,我们设置了一些具体的 seed. 自动训练具有不同初始化的多个模型并选择最佳模型非常有用。 或者,如果培训计划经常进行,并且在稍微不同的样本上进行(如数字双胞胎的情况),最好花更多时间创建更稳定的培训程序。 例如,您可以设置正弦学习速率控制或添加批量归一化。

结论

我们训练了一个用于分类的神经网络,并将其定位在画布上,作为模型内的另一个块。

训练过程的代码非常简洁,可以减少到7行。 块内的神经网络代码是自动生成的。

我们研究的用于训练神经网络的代码没有太多的*"超参数"*(由设计者配置的参数),它们可以手动排序,并且多层神经网络的表达能力非常高。

示例中使用的块