试题知识点智能标注

一. 项目结构

GitLab 项目地址
|- knowledge-automatic-tagging
|  |- ckpt            # 训练后的模型
|  |- data            # 经过预处理得到的结果数据
|  |- data_process        # 数据预处理代码
|  |- doc        # 项目细节文档
|  |- models           # 模型代码
|  |  |- word-cnn-concat    # CNN网络
|  |  |  |- network.py    # 模型网络结构
|  |  |  |- train.py      # 训练
|  |  |  |- predict.py     # 验证集/测试集预测
|  |  |- word-bigru-cnn-concat   # bigru + cnn 网络
|  |  |  |- network.py    # 模型网络结构
|  |  |  |- train.py      # 训练
|  |  |  |- predict.py     # 验证集/测试集预测
|  |  |- … # 其他待验证模型
|  |- notebook        # jupyter notebook代码
|  |- raw_data          # 题库提取的原始数据集
|  |- summary          # tensorboard 数据
|  |- tmp            # 临时文件夹,存放测试代码
|  |- data_helpers.py       # 数据处理函数
|  |- Dockerfile        # docker部署文件
|  |- evaluator.py        # 测评方案


二. 环境依赖

环境/库 版本
conda 4.5.4
python 3.5.2
jupyter notebook 4.2.3
tensorflow 1.7.0
tensorboard 1.7.0
word2vec 0.9.4
numpy 1.14.0
pandas 0.23.1
matplotlib 1.5.3

三. 模型架构

以 bigru + cnn 模型为例:


四. 数据采集

目前只采集了试题与知识点的映射数据,未考虑多维度标签,下面的文档说明以【初中数学】总集树为例:

all_knowledge_set

  • 存储内容:总集树中 node_type 为7的所有知识节点
  • 总行数:1154
  • 每行构成
    • 知识点id(node_id)
    • \t
    • 知识点名称
      1
      2
      3
      4
      5
      6
      7
      8
      2882	相反意义的量的定义
      5751 正数的定义
      11470 负数的定义
      5682 正数大于0,负数小于0,正数大于负数
      11409 0既不是正数,也不是负数
      2731 数轴的定义
      8689 原点的定义
      ...

knowledge_set

  • 存储内容:总集树中 node_type 为7的有试题挂靠的全部知识节点
  • 总行数:1093(部分知识点没有试题挂靠)
  • 每行数据成分
    • 知识点id(node_id)
    • \t
    • 知识点名称
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      21545	众数的定义
      18378 平行四边形的对角相等
      17047 最简分式的定义
      19227 抽样调查的定义
      17286 二次函数a<0,在对称轴右侧,y随x的增大而减小
      21781 两组直角边对应成比例的两个直角三角形相似
      17287 角的始边的定义
      9181 尺规作图:作线段的若干倍
      21306 总体的定义
      ...

question_title_set

  • 存储内容:初中数学集团题库与分公司题库的全部试题的题干
  • 总行数:49071(含有五道小题的试题会按一定规则拆分为五行)
  • 数据预处理:传入 contentTranslate 字段,提取 latex 公式,再去除文本中的括号、下滑线、空格等无用字符
  • 每行数据成分
    • 试题id
      • 非子母题:试题id
      • 子母题:主试题id + “-“ + 小题试题id
    • \t
    • 试题题干
      • 非子母题:试题题干
      • 子母题:主题干 + 小题题干
        1
        2
        3
        4
        5
        6
        7
        8
        9
        961	已知方程组\left\{\begin{align}&3x+my=5\\&x+ny=4\\\end{align})无解,m和n是绝对值小于10的整数,求m和n的值.
        98305 5的倒数是,A.-5,B.\frac{1}{5},C.\sqrt{5},D.5
        98304 -7的倒数为,A.7,B.\frac{1}{7},C.-\frac{1}{7},D.-7
        98307 一个数的倒数是它本身,则这个数一定是
        100354 已知|x-1|=2,|y|=3,且x与y互为相反数,求\frac{1}{3}{{x}^{2}}-xy-4y的值.
        98306 -2010的倒数是,A.2010,B.-\frac{1}{2010},C.\frac{1}{2010},D.-2010
        98309 若a+b=0,c和d互为倒数,m的绝对值为2,求代数式\frac{a+b}{a+b-c}+m^2-cd的值
        98308 有理数a等于它的倒数,有理数b等于它的相反数,则{{a}^{2002}}+{{b}^{2003}}=
        ...

question_explain_set

  • 存储内容:初中数学集团题库与分公司题库的全部试题的解答过程
  • 总行数:49071(含有五道小题的试题会按一定规则拆分为五行)
  • 数据预处理:传入 solutionProcessTranslate 字段,提取 latex 公式,再去除文本中的括号、下滑线、空格等无用字符
  • 每行数据成分
    • 试题id
      • 非子母题:试题id
      • 子母题:主试题id + “-“ + 小题试题id
    • \t
    • 解答过程
      1
      2
      3
      4
      5
      6
      7
      8
      9
      104961	∵方程组\left\{\begin{align}&3x+my=5\\&x+ny=4\\\end{align})无解,∴\frac{3}{1}=\frac{m}{n}≠\frac{5}{4}∴m=3n,4m\ne5n.又∵|m|=3|n|\lt10,∴|n|\lt\frac{10}{3}∴-\frac{10}{3}\ltn\lt\frac{10}{3},∴n的取值有-3,-2,-1,0,1,2,3;根据m=3n有①\left\{\begin{array}{l}m=-9\n=-3\end{array});②\left\{\begin{array}{l}m=-6\n=-2\end{array});③\left\{\begin{array}{l}m=-3\n=-1\end{array});④\left\{\begin{array}{l}m=0\n=0\end{array});⑤\left\{\begin{array}{l}m=3\n=1\end{array});⑥\left\{\begin{array}{l}m=6\n=2\end{array});⑦\left\{\begin{array}{l}m=9\n=3\end{array}).
      98305 ∵两个互为倒数的数乘积为1,∴5的倒数为\frac{1}{5},∴选,B.
      98304 ∵两个互为倒数的数乘积为1,∴-7的倒数为-\frac{1}{7},∴选,C.
      98307 解法一:∵两个互为倒数的数乘积为1,且一个数的倒数是它本身,∴这个数为1或-1.解法二:设这个数为x,这个数的倒数为\frac{1}{x},∵一个数的倒数是它本身,∴x=\frac{1}{x},∴x^2=1,∴x=±1.
      100354 ∵|x-1|=2,∴x-1=±2,∴x=3或x=-1,又∵|y|=3,∴y=±3,∵x与y互为相反数,∴x=3,y=-3,原式=\frac{1}{3}×3^2-3×(-3)-4×(-3)=3+9+12=24.
      98306 ∵两个互为倒数的数乘积为1,∴-2010的倒数为-\frac{1}{2010},∴选,B.
      98309 ∵c和d互为倒数,∴c·d=1且c≠0,又∵m的绝对值为2,∴m=±2,∴m^2=4,将a+b=0,c·d=1,m^2=4代入原式,得原式=\frac{0}{0-c}+4-1=0+4-1=3,∴代数式\frac{a+b}{a+b-c}+{{m}^{2}}-cd的值为3.
      98308 ∵有理数a等于它的倒数,∴a=\frac{1}{a},∴a=±1,又∵有理数b等于它的相反数,∴b=-b,∴b=0,∵{(±1)}^{2002}=1,0^{2003}=0,∴a^{2002}+b^{2003}=1+0=1.
      ...

question_knowledge_train_set

  • 存储内容:试题id与知识点id的对应关系
  • 总行数:49071
  • 每行数据成分
    • 试题id
      • 非子母题:试题id
      • 子母题:主试题id + “-“ + 小题试题id
    • \t
    • 知识点id,以逗号分隔
      1
      2
      3
      4
      5
      6
      7
      8
      9
      104961	14660,15999,18049,20520,16324,5842,5968,11602
      98305 4724
      98304 4724
      98307 4724
      100354 4445,22718,5264,10139
      98306 4724
      98309 4724,3758,5842
      98308 4724,5307,14333
      ...

五. 数据分析

1、知识点试题挂靠分析

知识点试题挂靠分布图
横坐标:共1093个值,代表每个知识点
纵坐标:知识点试题挂靠数

试题总数: 49071
知识点总数: 1093
试题挂靠数少于30的知识点约500个

  • 试题挂靠数 (0,10] 的知识点总数 280
  • 试题挂靠数 (11,20] 的知识点总数 133
  • 试题挂靠数 (21,30] 的知识点总数 89
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 试题挂靠数超过1000的知识点
知识点id 挂靠试题数 知识点名称
6361 4109 解一元一次方程
2672 3186 合并同类项的法则
11254 2309 勾股定理的内容
3432 2205 两直线垂直,夹角为90°
4278 2135 加法法则
8725 2122 去括号法则:如果括号外的因数是负数,去括号后原括号内各项的符号与原来的符号相反
14812 1819 乘法法则
4943 1720 三角形内角和180度
16324 1682 解二元一次方程组
10250 1676 等式两边乘一个数,或除以同一个不为0的数,结果仍相等
2775 1671 去括号法则:如果括号外的因数是正数,去括号后原括号内各项的符号与原来的符号相同
22102 1594 移项的定义
19276 1579 解一元二次方程
9450 1558 全等三角形对应边相等
15999 1513 负数的绝对值等于它的相反数
973 1477 直角三角形的定义
10028 1437 减法法则
12681 1413 直角三角形三边满足勾股定理
8994 1333 完全平方公式的定义
6673 1306 两直线平行,内错角相等
19568 1221 因式分解提公因式法的定义
6496 1090 等腰三角形等边对等角
22190 1059 平方的定义
9077 1013 两边及其夹角对应相等的两个三角形全等(SAS)

所有知识点挂靠详情:
知识点试题挂靠统计.txt

2、文本长度分析

题干长度分布如下(模型训练数据,题干取文本前220个字符):

解析长度分布如下(模型训练数据,解析取文本前600个字符):

3、文本内容分析

文本中部分字符模型无法识别,现汇总如下:
latex_replace.txt

文件详情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 匹配文本 \t 替换内容
\\right\| |
\\left\[ [
\\right\] ]
\\left\( (
\\right\) )
\\#xFF0C ,
\\#x2236 :
\\#xFF1A :
\\#x2235 ∵
\\#x2234 ∴
\\#x2237 ∷
\\#xFF05 %
\\#x22A5 ⊥
\\#x2299 ⊙
\\#x2295 ⊕
\\#x2287 超集或等于
\\#x2283 超集
\\#x2282 子集
\\#x226A 远小于
\\#x2026 …
\\#x25B5 △
\\Delta △
\\#x25B1 平行四边形
\\#x25A1 □
\\#x2218 °
\\#x221A √
\\#x2573 ╳
\\#x201C “
\\#x201D ”
\\#xFF0B +
.
.


六. 数据预处理

将题库采集到的数据解压至 /raw_data,并依次执行 /data_process 下各个py文件,不带任何参数。 各个py文件的输入与输出详情详见该目录下的 README.md

1. embed2ndarray.py

读取:sgns.baidubaike.bigram-char(百度百科词向量) 输出:data/word_embedding.npy、data/sr_word2id.pkl

中文词向量网站相关:

word_embedding.npy

  • 存储内容:sgns.baidubaike.bigram-char文件每行字符对应的词向量
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    word_embedding = np.load('../data/word_embedding.npy')
    print("type:\n",type(word_embedding))
    print("shape:\n",word_embedding.shape)
    print("前五行:\n",word_embedding[0:5])
    ------
    type:
    <class 'numpy.ndarray'>
    shape:
    (635976, 300)
    前五行:
    [[ -1.62635224e+00 -2.56559146e+00 3.39006690e-02 ..., -1.60284120e+00
    -1.48580317e+00 -2.40662751e-01]
    [ -1.22683971e+00 4.26988865e-01 6.59197586e-01 ..., -6.12283360e-01
    -1.43853570e+00 5.54417265e-01]
    [ -1.04500003e-01 -4.09622014e-01 2.50199996e-03 ..., 2.42422000e-01
    5.21025002e-01 3.80490012e-02]
    [ 5.04519999e-01 -1.70580000e-01 2.63853014e-01 ..., -2.24955007e-01
    -5.25089987e-02 3.17232013e-01]
    [ 1.16145998e-01 -5.88518977e-01 7.13479966e-02 ..., 2.72462010e-01
    3.07514995e-01 -2.88695008e-01]]
    ...

sr_word2id.pkl

  • 存储内容:sgns.baidubaike.bigram-char文件每行的char
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    sr_que2id = open('../data/sr_word2id.pkl','rb')
    sr_que2id_2 = pk.load(sr_que2id)
    print("type:\n", type(sr_que2id_2))
    print("shape:\n", sr_que2id_2.shape)
    print("前五行:\n", sr_que2id_2[:5])
    ------
    type:
    <class 'pandas.core.series.Series'>
    shape:
    (635976,)
    前五行:
    2 濲
    3 违法性
    4 莫斯基诺
    5 瓦尔纳
    6 少室
    dtype: object
    ...

2. question_and_topic_2id.py

读取:raw_data/question_knowledge_train_set.txt 输出:data/sr_question2id.pkl、data/sr_topic2id.pkl

sr_question2id.pkl

  • 存储内容:试题id-序号、序号-试题id
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    f = open('../data/sr_question2id.pkl','rb')
    sr_question2id = pickle.load(f)
    print("sr_question2id:")
    print("type:\n", type(sr_question2id))
    print("shape:\n", sr_question2id.shape)
    print("前五行:\n", sr_question2id[0:5])
    sr_id2question = pickle.load(f)
    print("\nsr_id2question:")
    print("type:\n", type(sr_id2question))
    print("shape:\n", sr_id2question.shape)
    print("前五行:\n", sr_id2topic[0:5])
    -----
    sr_question2id:
    type:
    <class 'pandas.core.series.Series'>
    shape:
    (100267,)
    前五行:
    104961 0
    98305 1
    98304 2
    98307 3
    100354 4
    dtype: int64
    sr_id2question:
    type:
    <class 'pandas.core.series.Series'>
    shape:
    (100267,)
    前五行:
    0 104961
    1 98305
    2 98304
    3 98307
    4 100354
    dtype: object

sr_topic2id.pkl

  • 存储内容:知识点id-序号、序号-知识点id
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    f = open('../data/sr_topic2id.pkl','rb')
    sr_topic2id = pickle.load(f)
    print("sr_topic2id:")
    print("type:\n", type(sr_topic2id))
    print("shape:\n", sr_topic2id.shape)
    print("前五行:\n", sr_topic2id[0:5])
    sr_id2topicd = pickle.load(f)
    print("\nsr_id2topic:")
    print("type:\n", type(sr_id2topic))
    print("shape:\n", sr_id2topic.shape)
    print("前五行:\n", sr_id2topic[0:5])
    -----
    sr_topic2id:
    type:
    <class 'pandas.core.series.Series'>
    shape:
    (1105,)
    前五行:
    6361 0
    2672 1
    11254 2
    4278 3
    3432 4
    dtype: int64

    sr_id2topic:
    type:
    <class 'pandas.core.series.Series'>
    shape:
    (1105,)
    前五行:
    0 6361
    1 2672
    2 11254
    3 4278
    4 3432
    dtype: object

3. word2id.py

读取:data/sr_word2id.pkl、raw_data/question_set.txt 输出:data/wd_train_content.npy

wd_train_content.npy

  • 存储内容:试题文本分词后每个字符对应的词向量的行号
  • 文本分词:jieba分词,精确模式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    word_embedding = np.load('../data/wd_train_content.npy')
    print("type:\n", type(word_embedding))
    print("shape:\n", word_embedding.shape)
    print("前五行:\n", word_embedding[0:5])
    -----
    type:
    <class 'numpy.ndarray'>
    shape:
    (100267,)
    前五行:
    [ list([631728, 113123, 508657, 282597, 508657, 361465, 508657, 102915, 361465, 235573, 470859, 514847, 1, 275510, 169616, 471680, 103939, 508657, 508657, 514847, 6883, 275510, 384350, 471680, 195329, 508657, 508657, 508657, 147259, 361465, 235573, 470859, 508657, 259781, 304875, 343493, 464651, 432604, 383956, 196920, 452500, 298593, 42433, 500032, 330228, 359327, 464651, 576705, 432604, 383956, 196920, 330228, 426877, 394166])
    list([103939, 330228, 244899, 452500, 146176, 103939, 508657, 7381, 361465, 483874, 470859, 361465, 103939, 470859, 508657, 223592, 361465, 103939, 470859, 103939])
    list([146176, 134472, 330228, 244899, 172615, 134472, 508657, 7381, 361465, 483874, 470859, 361465, 134472, 470859, 146176, 508657, 7381, 361465, 483874, 470859, 361465, 134472, 470859, 146176, 134472])
    list([357690, 553838, 330228, 244899, 452500, 382590, 381360, 464651, 191717, 498076, 553838, 15250, 452500])
    list([631728, 508657, 282597, 359410, 6883, 146176, 483874, 508657, 259781, 359410, 471680, 127874, 464651, 508657, 282597, 359410, 100969, 508657, 259781, 359410, 471680, 212700, 464651, 446506, 6883, 400506, 100969, 6063, 541193, 464651, 576705, 508657, 7381, 361465, 483874, 470859, 361465, 212700, 470859, 361465, 361465, 6883, 470859, 576696, 361465, 127874, 470859, 470859, 146176, 230989, 146176, 1, 330228, 426877, 394166])]

4. creat_batch_data.py

读取:data/wd_train_content.npy 输出:wd_train_path = ‘../data/wd-data/data_train/.npz’ wd_valid_path = ‘../data/wd-data/data_valid/.npz’ wd_test_path = ‘../data/wd-data/data_test/*.npy’

*.npy 文件

  • 存储内容:喂给模型训练的最终数据,固定seed,按照 batch_size(128) 进行打包
  • 每个batch文件包含128个标注样本,每个样本包括 X, y 两部分
  • X 表示每个样本的试题文本,这里对文本进行了截断,长度不足的用 0 padding到固定长度,y表示试题标注的知识点id
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    batch_data = np.load('../data/wd-data/data_train/0.npz')
    print("type:\n", type(batch_data))
    print(batch_data['X'].shape)
    print(batch_data['y'].shape)
    print(x[0:1])
    print(y[0:1])
    -----
    type:
    <class 'numpy.lib.npyio.NpzFile'>
    (128, 180)
    (128,)
    [[323920 264501 1 113123 7748 508657 282597 508657 361465 508657
    102915 361465 380901 470859 361465 341430 470859 1 146176 1
    471680 146176 103939 508657 1 275510 1 471680 133283 508657
    147259 361465 380901 470859 508657 259781 304875 394166 0 0
    0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0]]
    [list([213, 151, 7, 9, 59])]

七. 模型训练

  • 数据采集:2019年01月08号
  • 样本总数:49071(大题下的每个小题拆分为单独样本)
  • 分类总数:1093(知识点总数 1154,部分知识点没有挂靠试题)
  • 学段学科:初中数学
  • 分库来源:集团题库 + 分公司题库
  • 训练环境:一块 NVidia K80 GPU,内存17.2G
  • 训练时长:迭代全部样本 80 次,耗时约 小时
  • 测评结果:准确率 2.17726, 召回率 0.64506, F1值 0.497627(具体测评方案详见 evaluator.py)

训练过程中,训练集、验证集和测试按 8:1:1 进行拆分,模型目前尚未收敛,效果还可继续提升。

1
2
3
4
5
6
# 进入相应模型目录
cd models/word-cnn-concat-1/
# 训练
python train.py [--max_epoch 1 --max_max_epoch 6 --lr 1e-3 decay_rate 0.65 decay_step 15000 last_f1 0.4]
# 预测
python predict.py

八. 模型体验

请求链接http://knowledge.cce057087bb6b431593fffdb2d0b2ad41.cn-hangzhou.alicontainer.com
请求方式:POST
请求参数

  • title(对应试题详情接口 contentTranslate 字段)
  • explain(对应试题详情接口 explains 下的 solutionProcessTranslate 字段)

返回结果

  • title_parse(title 预处理后的文本)
  • explain_parse(explain 预处理后的文本)
  • predict
    • node_id(知识点 node_id)
    • node_name(知识点名称)
    • order(按知识点预测得分排序,序号越小得分越高)

模型说明:

  • 模型只支持 初中数学 知识点的预测,暂不支持多维度标签,不支持小学数学与高中数学,不支持其他总集树。
  • 初中数学知识树目前有61个知识点无试题挂靠,无法预测。知识点试题挂靠详情见数据分析模块
  • 理论上,一个知识点挂靠试题越多,预测越准确。后期随着数据量上升,准确率还会继续提升。
  • 预测知识点时,输入的题干文本需真实有意义,如果输入小学数学预测初中知识点,效果会比较差。

九. 后续调优

数据采集

  • 除了提取题干与知识点,还需提取解析,因为解析包含知识点部分特征信息

数据预处理

  • 剔除输入文本中的无用字符,转换latex公式中的错误字符,比如公式中的【/#xFF0C】转换为【,】。
  • 获取样本集中X长度概率分布,选择更合适的截断值。
  • 去除文本停用词(或者手动维护一个停用词表)
  • 针对数据量少,分布不均衡,采取上采样进行数据增强。文本分类常见方式为打乱语序、删除词语和同义词替换
  • 尝试剔除样本中的异常点,提升样本质量

网络结构

  • 由于数据量小,神经网络中间层尝试去掉dropout
  • 神经网络采用多标签分类,最后一层尝试用sigmoid替代softmax。
  • 优化评判标准,比如尝试用汉明损失替代精度、召回率和F1值。
  • 尝试其他模型,比如 rnn + bigru
  • 尝试腾讯词向量。目前由于内存原因,优先使用了体积较小的 1.6G 百度百科词向量,该词向量共计词汇635976个,用百度百科文本训练而成。但腾讯词向量更全面,也能匹配出更多的有效字符。
  • 尝试用BERT进行迁移学习,可能有奇效。今年自然语言最大突破是BERT,刷新11项纪录,其中就包括文本分类。

模型调参

模型调参耗时,需要大量工程经验,如果耗时一段时间没有结果,解决方案是接入今年开源的自动调参服务 AutoML。阿里目前有提供服务,效果未知。调参优先级为:

  • 最重要:学习率 α
  • 其次重要:β(动量衰减参数)、hidden unit(各隐藏层神经元个数)、mini-batch
  • 再次重要:β1、β2、ϵ(Adam 优化算法的超参数)、layers(神经网络层数)、decay_rate(学习衰减率)

十. 文章相关

评论