Engee 文档
Notebook

为神经网络生成C代码

在这个例子中,我们将探讨库的功能。 SymbolicsFlux 为各种神经网络创建独立的C代码-全连接,循环和卷积。

所需图书馆

首先,确保您安装了必要的库。

In [ ]:
Pkg.add(["Flux", "Symbolics"])
#Pkg.add("ChainPlots") # Временно исключаем эту библиотеку из-за несовместимости с последней версией Flux 
   Resolving package versions...
  No Changes to `~/.project/Project.toml`
  No Changes to `~/.project/Manifest.toml`

图书馆 Flux 允许您创建神经网络,其代码在语言中设置 Julia. 神经网络是用高级语言编写的,而不是在第三方库或DSL域特定语言PyTorch)中编写的,这一事实将允许我们使用多态机制并将神经网络转换为可以生成代码的数学表达式。

在将Flux库更新到最新版本之前,以下代码将产生无害的错误消息 Error during loading of extension DiffEqBaseZygoteExt... 这并不妨碍工作。 只需右键单击它并选择 Удалить выбранное.

In [ ]:
using Flux, Symbolics, ChainPlots
gr();

要小心,神经网络可以包含很多参数。 将包含数百或数千个参数的神经网络转换为数学表达式可能需要相当长的时间(几十秒),并且表达式本身不太可能便于感知。

从单个神经元生成代码

让我们使用库定义一个神经元的"神经网络 Flux 然后我们以数学方程的形式和与平台无关的代码的形式得到它。

我们将重复一次库的名称,以使其更清晰地准确使用它们

In [ ]:
using Flux
model = Dense( 1=>1, relu )
Out[0]:
Dense(1 => 1, relu)  # 2 parameters

这是一个非常简单的可配置算法,有两个参数,可以以符号形式看到,如下所示:

In [ ]:
using Symbolics
@variables x
model( [x] )
Out[0]:
$$ \begin{equation} \left[ \begin{array}{c} ifelse\left( 1.1927 x < 0, 0, 1.1927 x \right) \\ \end{array} \right] \end{equation} $$

激活函数 ReLU 可以预见的是,创建了一个具有分段线性描述的神经元。

该算法的所有系数都是随机选择的,但为了便于感知,我们将为它们分配一些值。

In [ ]:
model.weight .= [10]
model.bias .= [20]
model( [x] )
Out[0]:
$$ \begin{equation} \left[ \begin{array}{c} ifelse\left( 20 + 10 x < 0, 0, 20 + 10 x \right) \\ \end{array} \right] \end{equation} $$

让我们生成代码,看看会发生什么。:

In [ ]:
c_model_code = build_function( model( [x] ), [x]; target=Symbolics.CTarget(), fname="neural_net", lhsname=:y, rhsnames=[:x] )
println( """$c_model_code""" )
#include <math.h>
void neural_net(double* y, const double* x) {
  y[0] = ifelse(20.0f0 + 10.0f0 * x[0] < 0, 0, 20.0f0 + 10.0f0 * x[0]);
}

我们可以看到,至少,在代码中有一个非标准的功能。 ifelse,以及参数 double 他们有一个后缀 f0. 这些指令在库所针对的某些C标准中。 Symbolics 但是由于我们没有为自己配置代码生成,而是"开箱即用"生成代码,因此我们需要对其进行一些转换。 例如像这样:

In [ ]:
c_fixed_model_code = replace( c_model_code, "double" => "float", "f0" => "f" );

这样的代码已经可以放在模板中并编译。:

In [ ]:
c_code_standalone = """
#include <stdio.h>
#include <stdbool.h>

float ifelse(bool cond, float a, float b) { return cond ? a : b; }

$c_fixed_model_code

int main(int argc, char const *argv[]){
    float out[1];
    float in[] = {3.1415};
    
    neural_net( out, in );
    
    printf( "%f\\n", out[0] );
    return 0;
}""";

# Сохраняем в файл доработанный код
open("$(@__DIR__)/neural_net_fc.c", "w") do f
    println( f, "$c_code_standalone" )
end
In [ ]:
;gcc -o out_fc neural_net_fc.c -lm 
In [ ]:
;./out_fc
51.415001
In [ ]:
model( [3.1415] )
Warning: Layer with Float32 parameters got Float64 input.
  The input will be converted, but any earlier layers may be very slow.
  layer = Dense(1 => 1, relu)  # 2 parameters
  summary(x) = "1-element Vector{Float64}"
@ Flux /usr/local/ijulia-demos/packages/Flux/9PibT/src/layers/stateless.jl:60
Out[0]:
1-element Vector{Float32}:
 51.415

在这个阶段,您可能会收到警告,最好不要使用参数 Float64 在神经网络中,这不会有利于计算的准确性,但会损害性能(特别是在GPU上)。 将所有神经网络参数转换为 Float32 您可以使用命令 model = fmap(f32, model).

正如我们所看到的,使用C代码和使用Julia中指定的神经网络计算的值几乎相同,具有舍入精度。

从多层FC神经网络生成代码

让我们用一个由两个神经元组成的神经网络的例子来检查代码的生成。 目前,它将是一个常规的多层全连接神经网络(fc,全连接,有时称为感知器)。

In [ ]:
using Flux
mlp_model = Chain( Dense( 1=>2), Dense(2=>1) )
Out[0]:
Chain(
  Dense(1 => 2),                        # 4 parameters
  Dense(2 => 1),                        # 3 parameters
)                   # Total: 4 arrays, 7 parameters, 284 bytes.

下面是这个多层神经网络的说明。

In [ ]:
# Временно отключена из-за несовместимости версий библиотек ChainPlots и Flux
#using ChainPlots
#p = np = plot( mlp_model, titlefontsize=10, size=(300,300), markersize=8, xticks=((0,1,2),["Входной\nслой", "Скрытый\nслой", "Выходной\nслой"]), markerstrokewidth=2, linewidth=1 )

该模型使用随机值初始化,但它已经可以接受向量,矩阵等作为输入。 请注意,神经网络总是要求将参数向量传递给输入,即使它们是符号变量。

In [ ]:
using Symbolics
@variables x
c_model_code = build_function( mlp_model( [x] ), [x]; target=Symbolics.CTarget(), fname="neural_net", lhsname=:y, rhsnames=[:x] )
println( c_model_code )
#include <math.h>
void neural_net(double* y, const double* x) {
  y[0] = 0.1388071f0 * x[0];
}

**这个代码有什么问题?多层网络的表达式已简化为单个神经元的表达式。 应该是这样的。 图书馆 Symbolics 简化了代码生成前的表达式,仅由"线性层"组成的多层神经网络类似于单个神经元。 有必要在神经网络的第一层之后设置激活函数sigmoid, tanh...),除了线性(identity 或-跳过-)。

经过一些转换后,此代码可以放在一个块中。 C FunctionEngee画布,或向其添加函数 main 并将其编译成可执行的二进制文件。

从循环神经网络生成代码

循环神经网络被很好地用于时间序列分析和序列生成。 它们也可以是多层的,但它们具有稍微不同的神经元拓扑。 让我们看看我们得到什么样的代码。

In [ ]:
input_channels_nb = 2
hidden_state_size = 3
rnn = Flux.RNNCell(input_channels_nb, hidden_state_size)

# Эти параметры настраиваются в процессе обучения
# - для наглядности, назначим каждой матрице параметров некоторые значения
# - к сожалению, они не могут быть символьными
rnn.Wh .= 0.1 .+ reshape(1:hidden_state_size*hidden_state_size, hidden_state_size, hidden_state_size) ./ 10
rnn.Wi .= 0.01 .+ reshape(1:hidden_state_size*input_channels_nb, hidden_state_size, input_channels_nb) ./ 100
rnn.bias .= 0.001 .+ collect(1:hidden_state_size) ./ 1000

X = Symbolics.variables( :x, 1:input_channels_nb )
H = Symbolics.variables( :h, 1:hidden_state_size )

y,h = rnn( X ) # Этой командой можно выполнять нейросеть и одновременно обновлять состояние
println(y)
println(h)
Num[tanh(0.002 + 0.02x₁ + 0.05x₂), tanh(0.003 + 0.03x₁ + 0.06x₂), tanh(0.004 + 0.04x₁ + 0.07x₂)]
Num[tanh(0.002 + 0.02x₁ + 0.05x₂), tanh(0.003 + 0.03x₁ + 0.06x₂), tanh(0.004 + 0.04x₁ + 0.07x₂)]

注意,由于该拓扑中网络的输出是一个隐藏向量,因此计算的函数 y 而对于 h 它们是相同的。

In [ ]:
#p = plot( rnn_model, titlefontsize=10, size=(300,300), markersize=8, xticks=((0,1,2),["Входной\nслой", "Рекуррентный\nслой"]), markerstrokewidth=2, linewidth=1 )
In [ ]:
c_model_code = build_function( vcat(y,h), [X; H]; target=Symbolics.CTarget(), fname="neural_net", lhsname=:LHS, rhsnames=[:RHS] )
println( c_model_code )
#include <math.h>
void neural_net(double* LHS, const double* RHS) {
  LHS[0] = tanh(0.002f0 + 0.02f0 * RHS[0] + 0.05f0 * RHS[1]);
  LHS[1] = tanh(0.003f0 + 0.03f0 * RHS[0] + 0.06f0 * RHS[1]);
  LHS[2] = tanh(0.004f0 + 0.04f0 * RHS[0] + 0.07f0 * RHS[1]);
  LHS[3] = tanh(0.002f0 + 0.02f0 * RHS[0] + 0.05f0 * RHS[1]);
  LHS[4] = tanh(0.003f0 + 0.03f0 * RHS[0] + 0.06f0 * RHS[1]);
  LHS[5] = tanh(0.004f0 + 0.04f0 * RHS[0] + 0.07f0 * RHS[1]);
}

这个特定的体系结构返回一个状态向量作为输出变量。 h.

其他架构将需要更复杂的代码生成过程。 例如,可以使用通常的库工具训练模型。 Flux 但是对于代码生成阶段,将以Julia函数的形式开发替代神经网络架构(例如,将返回给我们的函数 yh).

从循环神经网络生成代码的改进数量可以使用以下模板进行估计,这很难称为简单:

In [ ]:
c_code_standalone = """
#include <stdio.h>

$(replace( c_model_code, "double" => "float", "f0" => "f" ))

int main(int argc, char const *argv[]){
    float out[3];
    float in[] = {10, 10, 10, 10, 10};
    
    for( int i=0; i<5; i++ ){
        neural_net( out, in );
        printf( "%f %f %f\\n", out[0], out[1], out[2] );
        
        /* Подставим выходное значения в входной вектор в качестве вектора состояния */
        in[2] = out[0];
        in[3] = out[1];
        in[4] = out[2];
    }
    
    return 0;
}""";

# Сохраняем в файл доработанный код
open("$(@__DIR__)/neural_net_rnn.c", "w") do f
    println( f, "$c_code_standalone" )
end

我们将编译并运行这个神经网络的代码。

In [ ]:
;gcc -o out_rnn neural_net_rnn.c -lm
In [ ]:
;./out_rnn
0.605636 0.717755 0.801931
0.041686 0.054232 0.066762
0.004701 0.006491 0.008282
0.002363 0.003464 0.004565
0.002217 0.003275 0.004332

我们使用相同的输入数据调用循环神经网络5次,每次得到不同的结果,因为该神经网络的状态向量已更新。

从卷积神经网络生成代码

In [ ]:
in_channels = 1
out_channels = 1
filter_x_size = 2
filter_y_size = 1
signal_size = 10

cnn_model = Chain(
    Conv((filter_x_size,filter_y_size), in_channels => out_channels, tanh; bias = false),
    Dense(signal_size - filter_x_size + 1 => 1)
    )
Out[0]:
Chain(
  Conv((2, 1), 1 => 1, tanh, bias=false),  # 2 parameters
  Dense(9 => 1),                        # 10 parameters
)                   # Total: 3 arrays, 12 parameters, 496 bytes.

为了可视化这个神经网络,我们需要提供一个输入向量的例子。 xs 具有正确的尺寸(宽度,高度,通道,补丁)。

In [ ]:
# xs = rand( signal_size );
# p = plot( cnn_model, reshape(xs, :, 1, 1, 1), titlefontsize=10, size=(300,300), markersize=8, xticks=((0,1,2),["Входной\nслой", "Сверточный\nслой", "Выходной\nслой"]), markerstrokewidth=2, linewidth=1 )

为了清楚起见,我们将神经网络的最后一层(FC)设置为权重和偏移量的某些值,我们将在生成的代码中看到这些值。

In [ ]:
cnn_model[2].weight .= reshape( collect(1:(signal_size - filter_x_size + 1)), 1, :);
cnn_model[2].bias .= [0.1];

该模型具有相当复杂的输入数据组织。 让我们演示如何做到这一点。:

In [ ]:
cnn_model( reshape(xs, :, 1, 1, 1) )[:] # w, h, channels, batch
Out[0]:
1-element Vector{Float32}:
 -23.33473

在这种情况下,我们只需将卷积操作实现为Julia函数即可。 当我们简单地尝试将符号变量的四维矩阵替换到模型中时,我们会得到一个错误 UndefRefError: access to undefined reference. 因此,让我们看看这个神经网络,并创建一个使用卷积核的参数的函数,我们可以从原始模型中获取全连接层。

In [ ]:
function my_convolution_net( c_in )
    c_out = [tanh( sum(c_in[i:i+1] .* reverse(cnn_model[1].weight[:]) )) for i in 1:length(c_in)-1]
    out = cnn_model[2]( c_out ); # Второй слой модели мы используем в нашем коде без изменений
    return out
end;

代码生成后会是什么样子?

In [ ]:
X = Symbolics.variables( :x, 1:signal_size )
c_model_code = build_function( my_convolution_net( X ), X; target=Symbolics.CTarget(), fname="neural_net", lhsname=:LHS, rhsnames=[:RHS] )
println( c_model_code )
#include <math.h>
void neural_net(double* LHS, const double* RHS) {
  LHS[0] = 0.1f0 + 7.0f0 * tanh(-0.72043973f0 * RHS[6] + -1.1665595f0 * RHS[7]) + 3.0f0 * tanh(-0.72043973f0 * RHS[2] + -1.1665595f0 * RHS[3]) + 6.0f0 * tanh(-0.72043973f0 * RHS[5] + -1.1665595f0 * RHS[6]) + 5.0f0 * tanh(-0.72043973f0 * RHS[4] + -1.1665595f0 * RHS[5]) + tanh(-0.72043973f0 * RHS[0] + -1.1665595f0 * RHS[1]) + 2.0f0 * tanh(-0.72043973f0 * RHS[1] + -1.1665595f0 * RHS[2]) + 8.0f0 * tanh(-0.72043973f0 * RHS[7] + -1.1665595f0 * RHS[8]) + 4.0f0 * tanh(-0.72043973f0 * RHS[3] + -1.1665595f0 * RHS[4]) + 9.0f0 * tanh(-1.1665595f0 * RHS[9] + -0.72043973f0 * RHS[8]);
}

从表面上看,代码看起来相当实用。 让我们把它放在我们建立的模板中,执行它,并将其与以前函数的结果进行比较。

In [ ]:
c_code_standalone = """
#include <stdio.h>
#include <stdbool.h>

float ifelse(bool cond, float a, float b) { return cond ? a : b; }

$(replace( c_model_code, "double" => "float", "f0" => "f" ))

int main(int argc, char const *argv[]){
    float out[1];
    float in[] = {$(join(xs, ","))};
    
    neural_net( out, in );
    
    printf( "%f\\n", out[0] );
    return 0;
}""";

# Сохраняем в файл доработанный код
open("$(@__DIR__)/neural_net_cnn.c", "w") do f
    println( f, "$c_code_standalone" )
end
In [ ]:
;gcc -o out_cnn neural_net_cnn.c -lm
In [ ]:
;./out_cnn
-23.334730
In [ ]:
cnn_model( reshape(xs, :, 1, 1, 1) )[:] # w, h, channels, batch
Out[0]:
1-element Vector{Float32}:
 -23.33473
In [ ]:
my_convolution_net( xs )
Out[0]:
1-element Vector{Float32}:
 -23.33473

显然,结果与舍入精度相似。

结论

我们已经从三种最流行的神经网络类型中生成代码:全连接(FC/MLP),循环(RNN)和卷积(CNN)。

仅对于完全连接的神经网络,代码生成相对简单直接,这使得可以显着自动化此过程。

在其他情况下,当自动创建神经网络代码时,从任务(1D或2D卷积,使用循环神经网络的隐藏层)开始是有用的。 通过配置CNN或RNN代码生成过程的手动步骤数量,很明显,自动化此过程需要修复特定场景。