K12应用题求解模型设计

773 阅读9分钟

概述

公司目标产品定义需求,需要生成应用题讲解步骤。而经大量的题型总结,K12应用题可以抽象为纯数学问题:

  1. 多轮一元一次方程求解(自定义名称):可通过知n求1的形式逐步求解
    如:已知宽高,求矩形面积
  2. 二元一次方程求解:可通过列举二元一次方程式求解答案
    如已知x + y = n, x - y = m, 求 x 或 y
  3. 带条件的二元一次方程求解:
    如方案类问题,已知 x + y = n, 当x * y 取最大值时,x 和 y 的取值是多少

而对于K12数学问题而言,通过知n求1的形式逐步求解的题目占比很高,且对于符合多轮一元一次方程求解的应用题,其讲解顺序与求解顺序一致,因此抽象为一类通用的数学模型求解意义重大。

由于从应用题题目的字符串 到 应用题的求解步骤 基于原项目的整体设计,经过了多个步骤处理,内容相对较多。此处仅介绍求解逻辑,即“多轮一元一次方程求解”数学模型的设计与实现

模型设计

类图设计

多轮一元一次方程问题HIL.png 其中红色框内是抽象的数学模型;蓝色框内为可扩展的场景内容 和 数学关系式集(数学描述方式)。

数学模型抽象

主要类对象

类参数公式:可重复使用的存储业务对象类为参数的公式类,以业务对象方法匹配参数
公式求解集:为了降低倒推公式的复杂性,保存公式各个参数倒推式对象
关系式 : 根据题目解析的数学关系,以名称匹配参数
求解式:单纯的公式NLG输出类
参数:用以匹配对象、公式

class 参数 {
    static 子类名称集 = {
        include: [],
        exclude: []
    }
 
    static test(str) {
        if (this.constructor === 参数) return false
 
        return this.子类名称集.include.some(val => {
                if (isRegExp(val)) return  val.test(str)
                else if (isString(val)) return val === str
                else throw Error(`Include 参数只支持 RegExp 和 String 类型`)
            }) && !this.子类名称集.exclude.includes(str)
    }
    // ...
}
主要算法
  1. 采用类深度搜索获取求解式的算法
// 多轮一元一次方程求解问题.js
function 获取解答求解式数组() {
    while (求解对象集 不为空) {
        // 1. 根据已知对象集,选择匹配的公式求解
        for (let 类参数公式 of this.类参数公式集) {
            if (类参数公式可应用于 已知参数 求解) {
                // 1.1 获取已知参数 和 待求参数的类
                // 1.2 从关联参数(公式、题目关系)中获取对象名称
                // 1.3 处理 获取已知参数 的限定条件,推出待求参数的限定
                // 1.4 如果不是求解对象集参数,重新生成已知参数
                //     如果是求解对象集参数,从求解对象集获取参数
                // 1.5 类参数公式 记录求解参数(避免再次应用公式)
                // 1.6 生成并记录求解式
            }
        }
        if (不可求解) {
        // 2. 根据题目私有关系求解
            for (let 私有关系式 of this.题目解析关系式集) {
                // 类似 1.,减少了1.2不必要操作
            }
        }
    }
}

2. 生成求解式集算法

// 多轮一元一次方程求解问题.js
// 根据求解式的求解项、参数项 倒推所有 有效的求解式 (待优化)
function 按求解集生成求解式集(求解式数组) {
    let res = []
    // 1. 可能有一题多问, 遍历求解集
    for (let 求解对象 of this.求解集) {
        // 2. 根据求解对象的已知参数倒推未知数
        let 未知对象数组 = [求解对象]
        while(未知对象数组.length > 0) {
            // 2.1 如果是 解题前的已知参数 ,跳过
            //     如果是 解题过程中产生的参数,保存至 未知对象数组,并保存该 求解式
            // 2.2 移除 该遍历的第一个 求解对象
        }
   }
}

3.根据参数实例匹配类参数公式算法

// 公式Handler.js
static 可应用类参数(类参数公式, 参数实例) {
    //(1)根据类参数公式,对参数实例进行分类
    // (2) 根据分类参数判断类参数公式是否可用(待优化)
    //     1. 如果存在多于一个类型的参数实例不存在,则不能应用该公式
    //     2. 排列组合选择 已知参数;如果无法选择足够的已知参数,则该公式不可用
}

输入流程设计

为弥补OCR识别准确率的不足,产品增加表单输入流程

image-20210816151556512.png

通过输入场景信息,判定特定场景,并根据场景属性(动词),生成对应的数学关系式,用以解析关系式(应用RegExp)

function 生成场景数学关系(场景) {
    return 数学关系数组.map(数学关系 => {...})
}

表单转换说明

一元一次方程求解中,都可以按特定规则(自定义正则)对参数进行数学模型转换:

  1. 类参数公式为已知对象之间的关系,因此可以直接以对象名称进行命名/匹配:
[
    '应纳税额 = 计税金额 * 适用税率',
    '计税金额 = 应纳税额 / 适用税率',
    '适用税率 = 应纳税额 / 计税金额'
]
  1. 题目解析关系式 可用文字解析为特定的数学关系,举例
消费税大约占售价的25%" => "消费税 = 售价 * 25%"

关系式可能与场景相关联,因此进行转换时需要通过特定的场景重新生成正则。通过正则表达式将文字输入转换为数学模型输入

// 目前暂时以Object类型表示
const 数学关系数组 = [
    {
        name: '含数字比例关系',
        // 形如 "/^(?<目标对象>.+?)等于(?<被乘数>.+?)乘以?(?<乘数>\D+)$/" 的正则捕获式
        regs: [],              
        // 将捕获对象生成为关系式、字符串参数数组、对象字符串数组
        transform: function({目标对象, 被乘数, 乘数}){
            return {
                 
            }
        }
    }
]

3. 基本类型的数值关系 表示格式为:[限定 +] 对象 + 数值 + 单位,举例

"11月份的计税金额20000元" =>
{
    限定: ["11月份"],
    对象: "计税金额",
    数量: "2000",
    单位: "元"
}

4. 求解集以自定义(如"多少")为标识区分名称和单位,格式为 [限定 +] 对象 + ”“ + 单位, 举例

"12月份的消费税多少元" =>
{
    限定: ["12月份"],
    对象: "消费税",
    单位: "元"
}

表单输入示例

const 多轮一元一次方程求解问题参数定义 = {
    /*
    *  @param {String} 扩展其他求解类型的标志位
    * */
    类型: "多轮一元一次方程求解问题",
    /*
    *  @param {String} 单独提取场景说明,用于动态提供特定场景的规范表达名称
    * */
    场景名称: "纳税场景",
    /*
    *  场景是正则捕获的场景相关内容, 因场景输入而异, 输入以对象为item的数组,目前用来做答题的说明
    *  @param {Array<Object>} 场景正则捕获的主谓宾 + 匹配的场景
    *  Object:
    *  @param {String} 类型 场景名称
    *  @param {String} 主体
    *  @param {String} 动作
    *  @param {String} 客体
    *  示例:
    *  TODO: 需要根据教案区分问题解答的主客体, 如"小明的饭店"中小明是需要在教案中展示的混淆主体(定语)
    * */
    场景: {[名称: "纳税场景",] 主体: "饭店", 动作: "缴纳", 客体: "营业税"},
    /*
    * 类参数公式是特定场景业务类的关系表示, 因业务场景而提前设定
    * @param {Array<Array<String>>} 可提供多个求解公式,要求各公式各参数完整的求解
    * 输入格式:
    * 示例:
    * [
    *    [
    *       '应纳税额 = 计税金额 * 适用税率',
    *       '计税金额 = 应纳税额 / 适用税率',
    *       '适用税率 = 应纳税额 / 计税金额'
    *    ]
    * ]
    * */
    类参数公式集: [],
    /*
    * 固定格式的公式求解式
    * @param {Array<String>} 规定格式的关系表达,需要定义各表达式的转换方式
    * 示例:箭头前为原始输入,需经过关系正则匹配并转换,箭头后为转换后格式
    * [
    *   "消费税大约占售价的25%", => 比例关系正则 => "消费税 = 售价 * 25%"
    * ]
    * */
    题目解析关系式集: ["消费税大约占售价的25%"],
    /*
    * TODO: 需要重新根据更多用例完善定义参数对象的数据结构
    *  如增加约束属性,区分"A的营业额" 和 "B的营业额", "10月的营业额"和"11月的营业额"
    * @param {Array<Object>} 参数对象数组
    * Object:
    * @param <String> 类型: 是特定场景下的业务类对象则为该类名称, 否则为“参数”
    * @param <String> 对象: 参数名称
    * @param <Number> 数值: 参数的数量
    * @param <String> 单位: 参数的单位
    * @param <String> uid: 唯一标识,暂时是约束的替代品,如多个“参数”类型情况下的区分的标识
    * 输入格式: [限定 + '的' +] 对象名称 + ['是'+ ] 数量 + 单位
    * */
    参数集: ["计税金额20000元"],
    /*
    * @param {Array<Object>} 同参数集对象定义,目前唯一区分是在“多轮一元一次方程求解问题”或其子类实例的不同属性中
    * 输入格式: [限定 + '的' +] 对象名称 + "多少"  + 单位
    * */
    求解集: ["消费税多少元"]
}

方案优缺点

扩展性

  1. 数学模型 和 业务场景 完全解耦

    多轮一元一次方程的整体设计将数学模型 和 业务场景完全解耦,因此可以应用通过编写特定场景的业务对象类、教案生成逻辑应用至不同场景

  2. 数学关系抽象

    抽象了数学关系类,可根据题目条件进行不同数学关系的匹配扩展

    (1)如数学公式扩展

        应纳税额 = 计税金额 * 适用税率
        应纳税额 = 计税金额 - 税后余额
    

    (2)如数学关系扩展

    AB5% => A = B * 5%
    AB10% => A = B * ( 1 + 10% )
    
  3. 业务对象增加限定属性(“约束”名称已在项目中存在),兼容多轮正则匹配

    经调研,中文表示的方式虽然多种多样,但是K9以内的数学应用题表达格式遵循基本的语法结构:
    因此,可以应用多轮正则 和 词语分类 进行对象转换,(多层次多轮正则匹配)初步设计如下:

1. 先匹配最短字段, 如 名词 + 名词,数量 + 名词
2. 后匹配形容词字段, 如 形容词 + 的 + 名词
3. 关系短语匹配,如A是B的5%,A占B的5%,按A的5%缴纳B(xx税)
4. 最后是自定义匹配, 如 关系短语匹配

待改进

  1. 算法改进:方法“可应用类参数”在“所有参数类型的参数实例都存在”的情况下,采用暴力求解
  2. 根据题意求解关系式 目前仅支持简单四则运算(一元一次的倒推式算法简答),目前不支持 多参数的 求解,例如:比例增量关系,a * (1 + 0.5) = b(已应用解决方案一解决)

解决方案:

  1. 应用类公式方法人为先写好倒推公式,而后进行参数映射、参数替换求解(目前实现方案)
  2. 写倒推算法:难