多年前我第一次在邮件组里看到“Neural Network in ABAP”这个话题时,第一反应是:这人大概把SAP系统玩坏了。毕竟ABAP是跑企业级业务逻辑的语言,处理的是物料凭证、销售订单、财务过账这类严谨活儿,神经网络这种靠矩阵求导和随机梯度下降吃饭的东西,怎么看都不像它该干的活。但后来我真把自己关在DEV系统里,用SE24写了两个类、折腾了一个能跑通XOR分类的三层网络之后,想法彻底变了——ABAP做神经网络,不是行为艺术,而是一件有实际价值的“跨界工具”。它适合做轻量级推理、概念验证,也可以在某些不允许外部依赖的SAP环境中完成简单的预测任务。这篇博文就把我踩过的坑、验证过的代码和调参心得全部拆开讲,给想尝试的ABAP开发者一条可以直接复现的路。需要说明的是,这不是让大家用ABAP去替代Python,而是搞清楚什么时候值得做、怎么做才不会把自己绕晕。
1. 为什么要在ABAP里做神经网络:场景与选型拆解
1.1 需求从哪来:SAP系统内做预测的实际动机
在我接触过的实际需求里,提出“ABAP里跑神经网络”的通常不是算法工程师,而是SAP顾问和企业内部的IT人员。原因很现实:企业的核心业务数据都在SAP里,有些预测场景——比如动态价格调整、交期估算、备件需求预测——如果每次都要把数据导出到外部Python环境训练和推理,会面临数据权限、接口维护、批处理调度等一系列麻烦。尤其是数据量不大但业务逻辑闭环要求高的场景,直接在ABAP层完成一个轻量模型,反而能让整个流程留在系统内部。
举个典型例子:某制造企业希望根据历史采购订单估算某种物料的下次采购提前期。特征字段包括供应商编号、物料组、采购数量、过去三个月的平均到货延迟。这种问题有几千条样本就够了,用单层或多层感知机就能做出可用的预测。如果企业没有成熟的数据科学平台,让ABAP顾问自己用ABAP实现一个带权重的预测函数,将模型参数存在透明表里,定期用批处理重新训练,是一条很务实的路。
还有一个隐藏动机是“免责式增强”。有些SAP标准功能里的逻辑没法直接改,但可以通过增强点调用自定义函数模块。如果把推理函数做成ABAP形式,集成到增强里几乎是零成本;反之,要调用外部HTTP服务还得考虑连接超时、SSL证书、字段映射等问题。
1.2 什么时候该用、什么时候千万别用
先泼一盆冷水:在ABAP里做神经网络,适用边界非常窄。如果任务满足下面几个条件,ABAP方案才有竞争力:
- 样本量在中低规模,比如几千到几万条,特征维度在几十以内。
- 预测逻辑需要嵌入到SAP业务流程中实时或准实时执行,且不方便对外发起同步调用。
- 团队里没有专职数据科学人员,但ABAP开发实力足够。
- 模型不需要频繁重训练,可以离线训练、线上推理。
反过来,如果企业已经有Python + 机器学习平台,或者数据量到了百万级、特征维度上千,又或者需要CNN、LSTM这类复杂结构,那就老老实实走外部服务。ABAP的数值计算效率、内存管理能力和生态都不适合干这种重活。我的经验是:超过一万条样本、超过30个特征,ABAP的训练耗时就会以分钟甚至小时计,这时候性价比就崩了。
另外有一个非常关键的点:不要尝试在ABAP里复刻框架级别的功能。ABAP没有自动微分引擎,也没有GPU加速,所有矩阵运算都要自己用内表写循环。这意味着,你写的不是“调包代码”,而是“算法代码”。如果对神经网络的数学原理不熟,会非常痛苦。所以我实现这个项目的首要目的其实是“教学验证”——用一个能跑通的极简网络,验证自己对前向传播和反向传播的理解,而不是为了生产级部署。
2. 先把神经网络按ABAP的习惯拆开
2.1 神经元、权重与激活函数:拿ABAP对象做类比
如果站在ABAP开发者的视角看神经网络,很多东西是可以“翻译”成熟悉概念的。一个神经网络的层,本质上就是一个权重矩阵加上一个偏置向量;一次前向传播,就是矩阵乘法外加逐元素的激活函数运算。这就像内表之间的乘法和行操作,只不过元素是浮点数。
用最经典的2-2-1网络举例:输入层2个节点,隐藏层2个节点,输出层1个节点。隐藏层的每个节点接到两个输入,分别乘以两个权重,再加一个偏置,然后过激活函数。这个结构如果拆成ABAP数据结构,大概长这样:
TYPES: ty_vector TYPE STANDARD TABLE OF f WITH EMPTY KEY, ty_matrix TYPE STANDARD TABLE OF ty_vector WITH EMPTY KEY. DATA: weights_ih TYPE ty_matrix, " 2 x 2 隐藏层权重 bias_h TYPE ty_vector, " 2 隐藏层偏置 weights_ho TYPE ty_matrix, " 1 x 2 输出层权重 bias_o TYPE ty_vector. " 1 输出层偏置权重矩阵的行和列含义必须心里有数:weights_ih的第一维是当前层的节点,第二维是上一层的节点。在代码里搞反维度,是所有索引越界的根源,后面我会反复强调。
激活函数我选择Sigmoid,因为它的导数形式简洁、输出范围在0到1之间,非常适合做二分类验证。Sigmoid在ABAP里写起来非常直接:
METHOD sigmoid. rv_result = 1 / ( 1 + exp( -1 * iv_x ) ). ENDMETHOD.exp是ABAP内置的e的幂函数,不需要额外引入任何包。导数也同样简单,只需要用输出值本身计算:
METHOD sigmoid_derivative. rv_result = iv_activation * ( 1 - iv_activation ). ENDMETHOD.2.2 前向传播:内表就是矩阵
前向传播是整个网络的核心推理过程。以2-2-1网络为例,输入向量是二维的,隐藏层输出向量的每个分量等于:对输入向量逐一乘权重、加偏置、再过Sigmoid。
用ABAP写前向传播,可以不用通用矩阵乘法,因为网络层数固定,节点数也固定,直接用双层循环反而更直观:
METHOD forward. DATA: lv_z_h TYPE f, lv_z_o TYPE f. FIELD-SYMBOLS: <fs_w> TYPE f. " 计算隐藏层: a_h[j] = sigmoid( sum_i( weights_ih[j][i] * x[i] ) + bias_h[j] ) CLEAR: hidden_layer. DO 2 TIMES. lv_z_h = bias_h[ sy-index ]. LOOP AT input_vector ASSIGNING FIELD-SYMBOL(<fs_x>). READ TABLE weights_ih INTO DATA(lv_w) INDEX sy-index. lv_z_h = lv_z_h + lv_w * <fs_x>. ENDLOOP. APPEND me->sigmoid( lv_z_h ) TO hidden_layer. ENDDO. " 计算输出层: y_pred = sigmoid( sum_j( weights_ho[1][j] * a_h[j] ) + bias_o[1] ) lv_z_o = bias_o[ 1 ]. LOOP AT hidden_layer ASSIGNING FIELD-SYMBOL(<fs_h>). READ TABLE weights_ho[ 1 ] INTO lv_w INDEX sy-tabix. lv_z_o = lv_z_o + lv_w * <fs_h>. ENDLOOP. output = me->sigmoid( lv_z_o ). ENDMETHOD.这里有一个ABAP开发容易忽略的点:READ TABLE ... INDEX适用于按索引读取标准表,但对二维内表中的行内表,要使用weights_ho[ 1 ]这种方式先取到第一行,再对该行做索引读取。如果版本较老,这行代码要拆成两个步骤:先READ TABLE weights_ho INTO DATA(ls_ho_row) INDEX 1,再READ TABLE ls_ho_row INTO lv_w INDEX sy-tabix。我在DEV系统里用的是740版本,新语法没问题,但给客户写增强时,还是得多留一手,先确认ABAP版本。
2.3 反向传播:链式法则在代码里的模样
反向传播是让很多ABAP开发头疼的部分,因为它涉及矩阵的转置、逐元素的乘法和链式求导。但只要我们回到本源,它其实就四个公式。
对于输出层,使用均方误差损失时,输出层误差可以被简化成:
delta_o = ( y_pred - y_true ) * ( y_pred * (1 - y_pred) )对于隐藏层,需要把输出层误差沿着权重回传到上一层:
delta_h[j] = ( sum_k( weights_ho[k][j] * delta_o[k] ) ) * ( a_h[j] * (1 - a_h[j]) )权重更新遵循梯度下降:
weights_ho[k][j] = weights_ho[k][j] - learning_rate * delta_o[k] * a_h[j] weights_ih[j][i] = weights_ih[j][i] - learning_rate * delta_h[j] * x[i]如果你接触过单位换算或者汇率重估这类ABAP批处理逻辑,会发现反向传播本质上就是“用误差反向分摊”的过程。每个权重应该调整多少,取决于它对最终误差的“责任比例”,这个责任比例就是梯度。理解到这一层,写代码就只是体力活了。
3. 从零实现一个2-2-1三层网络
3.1 数据与网络结构设计:用XOR验证算法正确性
我选择用XOR异或问题作为训练目标。原因有三:第一,XOR是非线性可分问题,单层感知机永远学不会,必须靠隐藏层引入非线性,用它验证反向传播最有效;第二,样本只有4条,训练过程肉眼可见,调试起来非常方便;第三,XOR 0/1分类结果直观,训练完成后预测值靠近0或1,一眼就能看出模型是否收敛。
训练样本长这样:
TYPES: BEGIN OF ty_sample, inputs TYPE ty_vector, target TYPE f, END OF ty_sample. DATA: lt_samples TYPE STANDARD TABLE OF ty_sample, ls_sample TYPE ty_sample. ls_sample-inputs = VALUE #( ( 0 ) ( 0 ) ). ls_sample-target = 0. APPEND ls_sample TO lt_samples. ls_sample-inputs = VALUE #( ( 0 ) ( 1 ) ). ls_sample-target = 1. APPEND ls_sample TO lt_samples. ls_sample-inputs = VALUE #( ( 1 ) ( 0 ) ). ls_sample-target = 1. APPEND ls_sample TO lt_samples. ls_sample-inputs = VALUE #( ( 1 ) ( 1 ) ). ls_sample-target = 0. APPEND ls_sample TO lt_samples.网络结构就是2-2-1,隐藏层激活函数Sigmoid,输出层激活函数Sigmoid,损失函数用均方误差。学习率我会先设为1.0,训练轮次2000次。这个配置跑下来,预测值一般能达到0.01以下和0.99以上,效果非常理想。
3.2 矩阵运算工具函数实现
虽然我们网络规模小,但为了代码整洁、方便扩展到更多节点,我建议在局部类里实现两个基础函数:矩阵乘法和逐元素更新。矩阵乘法是神经网络最核心的算子,前向传播和反向传播都离不开它。ABAP没有现成的矩阵库,我只能自己用内表写。下面是我实际用过的矩阵乘法实现:
METHOD multiply_matrices. DATA: lv_sum TYPE f. " io_a 维度: m x n, io_b 维度: n x p, 返回值维度: m x p DO m TIMES. DATA(ls_row) = VALUE ty_vector( ). DO p TIMES. lv_sum = 0. DO n TIMES. lv_sum = lv_sum + io_a[ sy-index ][ sy-index_from_a ] * io_b[ sy-index_from_a ][ sy-index ]. ENDDO. APPEND lv_sum TO ls_row. ENDDO. APPEND ls_row TO rv_result. ENDDO. ENDMETHOD.这段代码里,io_a[ sy-index ]这种写法在内表嵌套时比较危险,因为sy-index在内层循环会变化。稳妥做法是把行列索引先存到局部变量里。我实际更推荐直接把矩阵乘法写成专用方法,不追求通用性,因为通用矩阵乘法的索引管理在ABAP里容易把人绕晕。针对2-2-1这种固定结构,前向传播用2.2节那种直接循环更安全。
反向传播更新权重时,要让每个权重减去学习率乘以对应的梯度和激活值。这一步用嵌套循环写即可,也可以用DO ... TIMES配合字段符号更新内表值。我倾向于用FOR循环配合REFERENCE INTO,在740版本里很优雅:
LOOP AT weights_ho ASSIGNING FIELD-SYMBOL(<fs_row>). LOOP AT <fs_row> ASSIGNING FIELD-SYMBOL(<fs_w>) REFERENCE INTO DATA(lr_w). lr_w->* = lr_w->* - lr * delta_out * hidden_layer[ sy-tabix ]. ENDLOOP. ENDLOOP.注意,REFERENCE INTO拿到的是指针,必须用->*解引用才能修改原值。这个语法很多老ABAP开发不常用,但在这里写权重更新特别简洁。
3.3 ABAP类骨架:初始化与前向传播
我在SE24里创建了一个全局类ZCL_ABAP_NN,也可以直接用SE38里的局部类做快速验证。考虑到博文可读性,我用局部类展示核心逻辑。
CLASS lcl_simple_nn DEFINITION. PUBLIC SECTION. METHODS: constructor, train IMPORTING it_samples TYPE STANDARD TABLE OF ty_sample iv_epochs TYPE i iv_lr TYPE f, predict IMPORTING iv_x1 TYPE f iv_x2 TYPE f RETURNING VALUE(rv_pred) TYPE f. PRIVATE SECTION. DATA: weights_ih TYPE ty_matrix, bias_h TYPE ty_vector, weights_ho TYPE ty_matrix, bias_o TYPE ty_vector. METHODS: sigmoid IMPORTING iv_x TYPE f RETURNING VALUE(rv_result) TYPE f, sigmoid_derivative IMPORTING iv_activation TYPE f RETURNING VALUE(rv_result) TYPE f, forward IMPORTING iv_x1 TYPE f iv_x2 TYPE f EXPORTING ev_pred TYPE f ev_hidden TYPE ty_vector, backward IMPORTING iv_x1 TYPE f iv_x2 TYPE f iv_target TYPE f ev_pred TYPE f ev_hidden TYPE ty_vector. ENDCLASS.构造函数里做权重初始化。有一个非常关键的细节:不能把所有权重初始化为相同值,否则同一层的多个神经元会学到完全一样的权重,也就是“对称性问题”,等于隐藏层白设了。我用的初始化范围是-1.0到1.0之间的随机数。对Sigmoid网络来说,这个范围足够打破对称,又不会让加权输入过大导致梯度饱和。
METHOD constructor. DATA: lv_seed TYPE i. lv_seed = cl_abap_random=>seed( ). weights_ih = VALUE #( ( VALUE #( ( me->random( lv_seed ) ) ( me->random( lv_seed ) ) ) ) ( VALUE #( ( me->random( lv_seed ) ) ( me->random( lv_seed ) ) ) ) ). bias_h = VALUE #( ( me->random( lv_seed ) ) ( me->random( lv_seed ) ) ). weights_ho = VALUE #( ( VALUE #( ( me->random( lv_seed ) ) ( me->random( lv_seed ) ) ) ) ). bias_o = VALUE #( ( me->random( lv_seed ) ) ). ENDMETHOD.random方法里我用整型随机数除以1000来获得小数,简单粗暴但够用。如果嫌随机数质量不够,可以换成cl_abap_random_float,不过对XOR这种任务无伤大雅。
3.4 反向传播与权重更新实现
反向传播的内部逻辑我按“计算输出层误差 -> 计算隐藏层误差 -> 更新权重和偏置”三步走。直接看代码:
METHOD backward. DATA: lv_delta_o TYPE f, lv_delta_h TYPE ty_vector, lv_lr TYPE f.