项目要求

给定某个主观题与某个学生提交的解答,Aita 自动预测一个分值,分值的最大值是 max_score。 该预测可以帮我们迅速了解一个班级成绩的大体情况。

训练数据

为了提高预测的准确性与合理性, Aita 需要先从训练数据学习。 训练数据是 n 个解答与老师对这些解答的给分。 训练数据存储在一个纯文本文件 marked_answers.txt 中。 每个数据的格式如下所示:

1
2
3
4
5
6
7
8
9
10
@id
submission-date-and-time
Answer
#Marking Scheme
#Writing: 3/4
#Feasibility:1/2
#Creativity:1.5/2
#Potential impact:1/2
#Total:6.5/10

每个训练数据以@开始。

以井号开头的行是老师的改分标准。可以从Total:6.5/10提取该作业得分6.5。

输入与输出

aita -train的输入有两个文件与一个最大分值。 marked_answers.txt是包含解答与分数的训练数据。 question.txt是作业的问题。 aita -train结束后得到一个模型,模型的参数存储在ModelParameters.txt中。

aita -predict的输入是一个文件,answer.txt,即提交的作业答案,它只包含如下信息:

1
2
3
@id
submission-date-and-time
Answer

aita -predict的输出是:

1
@id Total:数字      

文件 answer.txt 中可含有多个人提交的作业解答。

注意

最简单的预测方法是所有的解答都给一个相同的成绩。

另外一种方法是做随机预测,即不看作业,随机给作业判定一个分数。比如说,从均值为7,方差为3的正态分布中随机选取一个值并取整,就得到这个作业的成绩。

我们希望 Aita 做得比相同成绩法或随机预测法在 test MSE(Mean Squared Error) 这个指标上至少好10%。

每个问题老师会有对应的改分标准,不同的问题改分标准是不一样的。 对每个问题,老师会先改十来个作业,给出这些作业的分值, 把这些作业与分值作为训练数据,把余下的作业作为预测数据。

要求分析

该大作业要求我们通过对已批改的作业进行训练,从而预测未批改的作业分数。

首先我们要寻找影响作业分数的因素,其次需要利用统计学习的方法进行分析,预测和优化,从而得到作业的预测分数。

预测变量

作业长度

个人发现作业长度可能影响writing的分数,因为对于过长的作业,老师的评语中会写到作业字数过多,导致分数较低。

作业句数

作业的总句数

平均句子长度

即作业的总字数除以作业句数

关键词匹配度

首先会建立起关键词库,比较得出关键词出现次数

代码实现

由于python编程能力不足,参考了飞机的python代码,参考链接:

https://github.com/Bi0x/Aita

python前提

jieba库

jieba是优秀的中文分词第三方库

1
2
3
4
5
6
7
8
jieba.setLogLevel(logging.INFO) #关闭jieba结巴分词日志输出
jieba.cut(s) #精准模式,返回一个可迭代的数据类型
jieba.cut(s,cut_all=True) #全模式,输出文本s中所有可能单词
jieba.cut_for_search(s) #搜索引擎模式,适合搜索引擎建立索引的分词结果
jieba.lcut(s)
jieba.lcut(s,cut_all=True) #全模式,返回一个列表类型
jieba.lcut_for_search(s) #搜索引擎模式,返回一个列表类型
jieba.add_word(w) #向分类词典中增加新词

collections

这个模块实现了特定目标的容器,以提供Python标准内建容器 dict、list、set、tuple 的替代选择。

  • Counter:字典的子类,提供了可哈希对象的计数功能
  • defaultdict:字典的子类,提供了一个工厂函数,为字典查询提供了默认值
  • OrderedDict:字典的子类,保留了他们被添加的顺序
  • namedtuple:创建命名元组子类的工厂函数
  • deque:类似列表容器,实现了在两端快速添加(append)和弹出(pop)
  • ChainMap:类似字典的容器类,将多个映射集合到一个视图里面

Counter

Counter是一个dict子类,主要是用来对你访问的对象的频率进行计数。
常用方法:

  • elements():返回一个迭代器,每个元素重复计算的个数,如果一个元素的计数小于1,就会被忽略。
  • most_common([n]):返回一个列表,提供n个访问频率最高的元素和计数
  • subtract([iterable-or-mapping]):从迭代对象中减去元素,输入输出可以是0或者负数
  • update([iterable-or-mapping]):从迭代对象计数元素或者从另一个 映射对象 (或计数器) 添加。

csv

由于用文本文件存放关键词库和提取关键词有一些编码等相关问题,所以用csv格式存放

sklearn

Scikit-learn(sklearn)是机器学习中常用的第三方模块,对常用的机器学习方法进行了封装,包括回归(Regression)、降维(Dimensionality Reduction)、分类(Classfication)、聚类(Clustering)等方法。

对于该项目,本人主要采用了多元线性回归,KNN,回归树,随机森林,向量机等方法进行训练与预测,发现随机森林和支持向量回归预测较为准确,由于大佬用的是随机森林,所以本人采用支持向量回归进行数据预测以及分析。

向量机实现例子如下:

1
2
3
4
5
6
7
8
from sklearn import svm
X = [[0, 0], [2, 2]]
y = [0.5, 2.5]
regr = svm.SVR()
regr.fit(X, y)
SVR()
regr.predict([[1, 1]])
array([1.5])

re

正则表达式是一个特殊的字符序列,它能帮助你方便的检查一个字符串是否与某种模式匹配。

Python 自1.5版本起增加了re 模块,它提供 Perl 风格的正则表达式模式。

re 模块使 Python 语言拥有全部的正则表达式功能。

compile 函数根据一个模式字符串和可选的标志参数生成一个正则表达式对象。该对象拥有一系列方法用于正则表达式匹配和替换。

re 模块也提供了与这些方法功能完全一致的函数,这些函数使用一个模式字符串做为它们的第一个参数。

正则表达式可参考:https://www.cnblogs.com/shenjianping/p/11647473.html

我们需要通过正则表达式读取作业数据并进行内容的分离。

WordAnalyser.py

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#! /usr/bin/python3
import csv
import jieba, collections, logging

jieba.setLogLevel(logging.INFO)

class WordAnalyser:

def read_remove_word(self):
# ? 获取需要剔除的字符和词语
fp = open('./assets/remove_words.csv', 'r')
csv_loader = csv.reader(fp)
return [eval(i).decode('utf-8') for i in list(csv_loader)[0]]

def create_remove_words(self):
# ? 添加需要剔除的关键词
remove_words = [
u'的', u',', u'和', u'是', u'随着',
u'对于', u'对', u'等', u'能', u'都',
u'。', u' ', u'、', u'中', u'在',
u'了', u'通常', u'如果', u'我们', u'2',
u'需要', u'\n', u'.', u':', u',',
u',', u'(', u')', u'与', u'有',
u'会', u'也', u'以及', u'可', u'通过',
u'上', u'可以', u'并', u"\u3000", u'1',
u'to', u'\t', u'一个', u'将', u'到',
u'“', u'”', u'不', u'地', u'in',
u'于', u'还', u'我', u'人', u'为',
u'更', u'就',
]
fp = open('./assets/remove_words.csv', 'w')
csv_saver = csv.writer(fp)
encode_remove_words = []
# 把常用词添加进去
for i in remove_words:
encode_remove_words.append(i.encode('utf-8'))
csv_saver.writerow(encode_remove_words)
fp.close()

def keyword_analyse(self, answers):
#关键词分析
remove_words = self.read_remove_word()
word_objects = []
for answer in answers:
word_list = jieba.cut(answer, cut_all=False)
for i in word_list:
#非常用词,添加次数
if i not in remove_words:
word_objects.append(i)
word_counts = collections.Counter(word_objects)
#提取出十个最常用的关键词
word_counts_tops = word_counts.most_common(10)
print(word_counts_tops)
return word_counts_tops

if __name__ == '__main__':
word_analyser = WordAnalyser()
print("------->>> 目前剔除的词如下: <<<-------")
print(word_analyser.read_remove_word())

Main.py

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
#! /usr/bin/python3
# encoding:utf-8
from WordAnalyser import WordAnalyser
from sklearn.ensemble import RandomForestRegressor,GradientBoostingClassifier
from sklearn import linear_model,tree,svm
from sklearn.linear_model import SGDClassifier
from sklearn.kernel_ridge import KernelRidge
import re, math
import random

class Aita:
def __init__(self):
super().__init__()
word_analyser = WordAnalyser()
self.show_question_name()
self.answers_chunk = self.file_reader('./assets/marked_answers.txt') #每个人的作业与评分
self.answers_score = self.get_total_score(self.answers_chunk) #每个人的作业分数
self.answers_main = self.get_answers_main(self.answers_chunk) #每个人的作业内容
self.answers_len = self.get_answers_len(self.answers_chunk) #每个人的作业内容的长度
self.sentences_counts = self.get_sentences_counts(self.answers_chunk) #每个人的作业内容的句子数量
self.averagesentences_counts= self.get_averagesentences_counts(self.answers_len,self.sentences_counts,self.answers_chunk) #每个人的作业内容的平均句长
self.top_words = word_analyser.keyword_analyse(self.answers_main)
self.answers_keywordcount = self.get_keyword_counts(self.top_words, self.answers_main) #每个人的作业的关键词出现个数
self.classifier = None

def show_question_name(self):
#显示问题
fp = open('./assets/question.txt',encoding='utf-8')
print("问题:《" + fp.readline()[:-1] + "》")
fp.close()

def file_reader(self, path):
#读取答案数据,并将答案分组
train_file = open(path,encoding='utf-8')
file_text = "".join(train_file.readlines())
regex_pattern = r'(\d{12}.*?#Total: *?\d\.*\d*/10)' #获取答案数据
reg = re.compile(regex_pattern, re.DOTALL)
return reg.findall(file_text)

def get_total_score(self, answers):
#分析答案,获取评分
total_score = []
regex_pattern = r'#Total: *?(\d\.*\d*)/10' #获取totaal分数
reg = re.compile(regex_pattern, re.DOTALL)
for answer in answers:
total_score.append(float(reg.findall(answer)[0]))
return total_score

def get_answers_main(self, answers):
#主要回答内容
answers_main = []
regex_pattern = r':\d\d\n(.*)?#Marking Scheme' #获取作业内容
reg = re.compile(regex_pattern, re.DOTALL)
for answer in answers:
answers_main.append(reg.findall(answer)[0])
return answers_main

def get_answers_len(self, answers):
#回答长度
answers_len = []
for answer in answers:
answers_len.append(len(answer))
return answers_len

def get_sentences_counts(self,answers):
#句子个数
sentences_counts = []
for answer in answers:
sc=answer.count(".")+answer.count("。")+answer.count("?") +answer.count("?")+answer.count("!")+answer.count("!")
if(sc==0):
sc=1
sentences_counts.append(sc)
print(sentences_counts)
return sentences_counts

def get_averagesentences_counts(self,answers_len,sentences_counts,answers):
#平均句长
averagesentences_counts=[]
for i in range(len(answers_len)):
averagesentences_counts.append(answers_len[i]/sentences_counts[i])
print(averagesentences_counts)
return averagesentences_counts

def get_keyword_counts(self, keywords, answers):
#获取关键词
keyword_counts = []
for answer in answers:
per_keyword_counts = []
for i in keywords:
per_keyword_counts.append(answer.count(i[0]))
keyword_counts.append(per_keyword_counts)
return keyword_counts

def main_trainer(self):
train_data = []
for i in range(len(self.answers_main)):
per_line = [self.answers_len[i],self.sentences_counts[i],self.averagesentences_counts[i]]
per_line.extend(self.answers_keywordcount[i])
train_data.append(per_line)
print("------>>> 正在训练模型 <<<------")
'''
使用了多种模型进行训练,发现NuSVR效果相对较好。
'''
#clf = RandomForestRegressor(max_depth=10, n_estimators=1000, min_samples_split=2)
#clf = GradientBoostingClassifier(n_estimators=100, learning_rate=0.1,max_depth=1, random_state=0, loss='ls')
#clf=linear_model.LinearRegression(fit_intercept=True,normalize=True)
#clf=KernelRidge(alpha=1.0,kernel='rbf',gamma=5**-2)
#clf=tree.DecisionTreeRegressor()
#clf = SGDClassifier(loss="hinge", penalty="l2", max_iter=12)
#clf=svm.SVR()
clf=svm.NuSVR(nu = 0.5)
clf.fit(train_data, self.answers_score)
self.classifier = clf
print("------>>> 训练模型结束 <<<------")

def predict(self, predict_answers):
predict_keywords_count = self.get_keyword_counts(self.top_words, [predict_answers])
predict_data = self.get_answers_len([predict_answers])
predict_data.extend(self.get_sentences_counts([predict_answers]))
predict_data.extend([self.get_answers_len([predict_answers])[0]/self.get_sentences_counts([predict_answers])[0]])
predict_data.extend(predict_keywords_count[0])
return self.classifier.predict([predict_data])[0]

#End
def score_optimization(predict_res):
#优化分数,因为打分分数都是整数或者0.5分制的
decimal=predict_res%1
natural=predict_res-decimal
if(decimal<0.25):
predict_res=natural
elif(decimal<0.75):
predict_res=natural+0.5
elif(decimal<1):
predict_res=natural+1
if(predict_res<0):
predict_res=0
if(predict_res>10):
predict_res=10
return predict_res

def run():
aita = Aita()
aita.main_trainer()
predict_chunk = aita.file_reader("./assets/marked_answers2_simple.txt")
predict_true_score = aita.get_total_score(predict_chunk) #获取要预测分数的作业的真实分数
predict_main = aita.get_answers_main(predict_chunk) #获取要预测分数的作业
mean_diff = 0
rss = 0
mean_diff2 = 0
rss2=0
for i in range(len(predict_main)):
predict_res = aita.predict(predict_main[i])
print("\n----------------------------")
print("预测结果: " + str(predict_res))
predict_res=score_optimization(predict_res)
print("优化结果: " + str(predict_res))
ran=round(random.uniform(6.5,8.5),1)
#ran=7
print("随机结果: " + str(ran))
rss2 += (predict_true_score[i] - ran) ** 2
mean_diff2 += abs(predict_true_score[i] - ran)
print("实际结果: " + str(predict_true_score[i]))
print("----------------------------")
rss += (predict_true_score[i] - predict_res) ** 2
mean_diff += abs(predict_true_score[i] - predict_res)
mse = rss / len(predict_main)
mse2 = rss2 / len(predict_main)
print("平均误差: " + str(mean_diff / len(predict_main)))
print("MSE: " + str(mse))
print("----------------------------")
print("random:平均误差: " + str(mean_diff2 / len(predict_main)))
print("random:MSE: " + str(mse2))

if __name__ == '__main__':
run()

实验数据与分析

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
38
39
#NuSVR
----------------------------
平均误差: 0.6702127659574468
MSE: 0.6329787234042553
----------------------------
random:平均误差: 0.7914893617021279
random:MSE: 0.9242553191489364

#SVR
----------------------------
平均误差: 0.8617021276595744
MSE: 0.9946808510638298
----------------------------
random:平均误差: 0.7787234042553192
random:MSE: 0.9804255319148939

#tree.DecisionTreeRegressor
----------------------------
平均误差: 1.6914893617021276
MSE: 3.632978723404255
----------------------------
random:平均误差: 0.6893617021276597
random:MSE: 0.6957446808510639

#RandomForestRegressor
----------------------------
平均误差: 0.6382978723404256
MSE: 0.7872340425531915
----------------------------
random:平均误差: 0.8340425531914895
random:MSE: 1.0119148936170212

#linear_model.LinearRegression
----------------------------
平均误差: 1.6170212765957446
MSE: 4.638297872340425
----------------------------
random:平均误差: 0.7851063829787234
random:MSE: 0.8938297872340427

在进行模型选择中,可以发现,只有NuSVR和RandomForestRegressor的预测效果比经过一点点“优化”的随机数好。。。

如果作业全部打7分,比较预测准确度的话。。。

1
2
3
----------------------------
random:平均误差: 0.6382978723404256
random:MSE: 0.7659574468085106

离谱

支持向量机

支持向量机的特点

  1. SVM的最终决策函数只由少数的支持向量所确定,计算的复杂性取决于支持向量的数目,而不是样本空间的维数,这在某种意义上避免了“维数灾难”。
  2. 少数支持向量决定了最终结果,这不但可以帮助我们抓住关键样本、“剔除”大量冗余样本,而且注定了该方法不但算法简单,而且具有较好的“鲁棒性”。
  3. SVM在小样本训练集上能够得到比其它算法好很多的结果。SVM优化目标是结构化风险最小,而不是经验风险最小,避免了过拟合问题,通过margin的概念,得到对数据分布的结构化描述,减低了对数据规模和数据分布的要求,有优秀的泛化能力。
  4. 它是一个凸优化问题,因此局部最优解一定是全局最优解的优点。

由于本次实践作业样本较小,是小样本训练集,而且SVM可以帮助我们抓住关键样本,所以预测较为准确。