项目要求 给定某个主观题与某个学生提交的解答,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
的输出是:
文件 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.cut(s) jieba.cut(s,cut_all=True ) 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 svmX = [[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 import csvimport jieba, collections, loggingjieba.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 from WordAnalyser import WordAnalyserfrom sklearn.ensemble import RandomForestRegressor,GradientBoostingClassifierfrom sklearn import linear_model,tree,svmfrom sklearn.linear_model import SGDClassifierfrom sklearn.kernel_ridge import KernelRidgeimport re, mathimport randomclass 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' 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=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 ] def score_optimization (predict_res ): 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 ) 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 ---------------------------- 平均误差: 0.6702127659574468 MSE: 0.6329787234042553 ---------------------------- random:平均误差: 0.7914893617021279 random:MSE: 0.9242553191489364 ---------------------------- 平均误差: 0.8617021276595744 MSE: 0.9946808510638298 ---------------------------- random:平均误差: 0.7787234042553192 random:MSE: 0.9804255319148939 ---------------------------- 平均误差: 1.6914893617021276 MSE: 3.632978723404255 ---------------------------- random:平均误差: 0.6893617021276597 random:MSE: 0.6957446808510639 ---------------------------- 平均误差: 0.6382978723404256 MSE: 0.7872340425531915 ---------------------------- random:平均误差: 0.8340425531914895 random:MSE: 1.0119148936170212 ---------------------------- 平均误差: 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
离谱
支持向量机 支持向量机的特点
SVM的最终决策函数只由少数的支持向量所确定,计算的复杂性取决于支持向量的数目,而不是样本空间的维数,这在某种意义上避免了“维数灾难”。
少数支持向量决定了最终结果,这不但可以帮助我们抓住关键样本、“剔除”大量冗余样本,而且注定了该方法不但算法简单,而且具有较好的“鲁棒性”。
SVM在小样本训练集上能够得到比其它算法好很多的结果。SVM优化目标是结构化风险最小,而不是经验风险最小,避免了过拟合问题,通过margin的概念,得到对数据分布的结构化描述,减低了对数据规模和数据分布的要求,有优秀的泛化能力。
它是一个凸优化问题,因此局部最优解一定是全局最优解的优点。
由于本次实践作业样本较小,是小样本训练集,而且SVM可以帮助我们抓住关键样本,所以预测较为准确。