读别人的代码像破解密码一样,乐趣无穷。
高考小强是一个基于Python2的、关于高考志愿填报的问答系统,它收集了往年的各个学校、各个专业的分数信息,可以根据省份、分数、文理推荐合适的学校。
一、五大板块
- CollegeRecommendation
学校、专业推荐模块,通过SQL查询数据库获得推荐的学校列表、专业列表 - DialogueManagement
对话管理模块,将格式化数据转化为具体的句子 - InformationExtraction
查询抽取模块 - InformationRecognition
查询识别模块 - 主调用模块
调用其他模块
不同模块之间以JSON的形式传递数据。
二、DialogueManagement:对话管理模块
本模块是离用户最近的模块,包含两个类:
- ReadOrWriteWithConsole:控制台读入写出类,涉及到UTF8编码的转换,这个类可有可无,不太重要。
- Response类,这个类是本模块的核心,下面重点介绍这个类。
Response类有如下几个静态函数,每个函数都表示高考小强能说出的一类话。
- initial_ask():初始的问候语
- normal_inquire_response(collegelist):正常询问回答,传入参数为collegelist,即推荐的学校列表
- normal_major_response(major_list):把推荐的专业列表告知用户,“据小强分析,可以考虑的报考方向或专业有”
- re_ask_lack_attribute(lack_tag):缺少属性回答,lack_tag表示缺少的属性,用户必须提供省份、文理、分数等信息,如果没有提供全,就要再次询问用户
- more_restriction():是否有更多限制,比如用户说“我想去北京上大学”,就要限制为学校是北京的大学。
- could_to_some_college(tag, college, now_type):能否上某某大学。
- ambiguous_school(base, school_list):模糊学校,比如用户说“我想上东大”,无法判断是东南大学还是东北大学
- i_donnot_know():小强认怂,“抱歉,小强的数据不够充分,暂时不能预测”
- what_function():返回功能简介。比如用户问“有什么功能”。
- too_big_score():“您的分数太高了,吹牛不是好习惯”
下面详细介绍各个函数。
initial_ask()
为了假装自己很灵活,写了5个问候语,随机选一个。这种方法在本系统中大量使用。
打印完问候语之后,就要开始干活了:问问用户的省份、文理、分数。
def initial_ask():seed = random.randint(0, 4)re = []随机选取5个问候句if seed == 0:re.append(u'您好,我是人工智能小强,专注于高考志愿填报')elif seed == 1:re.append(u'您好,我是高考志愿填报助手小强')elif seed == 2:re.append(u'很高兴见到你,我叫小强')elif seed == 3:re.append(u'Hello!我是小强!')elif seed == 4:re.append(u'高考志愿填报助手--小强,竭诚为您服务!')re.append(u'小强是根据往年数据,结合分数与排名为您推荐学校,推荐结果仅供参考!')re.append(u'请问您有什么和高考志愿填报相关的需求?')re.append(u'输入省份、分数和文理即可开始挑选学校啦!')return re
normal_inquire_response(collegelist)
collegelist是一个如下结构的JSON
{'low':[('东北大学',628,'本科提前批'),('华中科技',630,'本科提前批')],'mid':[('北航',650,'本科提前批'),('西安交大',649,'本科提前批')],'high':[('清华大学',680,'本科提前批')]
}
此函数的作用就是把这个学校列表转化为一个字符串告知用户。
re_ask_lack_attribute(lack_tag)
lack_tag可能的取值:
- origin:省份
- type:文理
- score:分数
根据缺失的属性,小强会问你“您的[省份|文理|分数]是什么?”,此函数又是写了多个问法模板随机选一个,以避免让人觉得死板。
more_restriction():还有其他要求没有
def more_restriction():seed = random.randint(0, 1)re = u''if seed == 0:re = u'请问还有什么其他要求吗?'elif seed == 1:re = u'还需要做什么筛选吗?'return [re]
could_to_some_college(tag, college, now_type)
此函数用于回答“我能不能上北大”这样的问题。
tag表示小强的观点,分为四类:
- 完全可以
- 有把握
- 很有可能
- 不可能
college表示用户想要上的学校,now_type表示专业,这两个参数都是在拼接回复的时候用到。
ambiguous_school(base, school_list)
base=‘东大’,school_list=['东北大学','东南大学']
三、CollegeRecommendation模块:数据发生的地方
DialogueManagement只是将结构化数据转化为文本,没做什么大事。
InformatioonExtractor和InformationRecognition只是解析用户输入,也没做什么大事。
CollegeRecommendation模块则是系统的核心,是真正涉及到数据处理的地方。
本系统使用的是MySQL数据库,CollegeRecommendation的作用就是执行SQL语句去数据库里面查询。
下面首先介绍一下数据库设计。
分数名次表
score_rank表的结构
- origin:省份
- type:文理
- year:年份
- score:分数
- rank:名次
分数-学校表
- origin:省份
- type:文理
- year:年份
- average_score:平均分
- min_score:最低分
- min_rank:最低分全省名次
- batch:批次
分数-专业表
- origin
- type
- year
- average_score
- major
- school
- batch
饭得一口一口吃,事得一件一件做。先看recommend_school
predict_school(origin, type, score,school)
判断能不能上某学校
参数: origin:省份(不含"省"字,如"山东""新疆""西藏"), type:"文科"或 "理科", score:分数, school:学校名
返回值:
{'result':0/1/2/3(基本不可能/可能性较小/有把握/太亏),'school_score':学校预测平均分, 'school_rank':学校最低分排名, 'student_rank':学生排名},各项若为-1则是缺少数据,数据不足以做出判断则返回None
根据分数、省份、文理获取省内排名select rank from score_rank where origin = %s and type = %s and year = 2016 order by abs(score - %s) limit 1 ',(origin,type,score)
根据省份、文理、学校、年份获得学校的平均分、最低分。
select average_score,min_score,min_rank from school_score where student_origin = %s and student_type = %s and school = %s and year = 2016',(origin,type,school)
高考小强观点的产生,根据学校的平均分、最低分综合判断
# 先根据线上分判断if sch_ave_score > 0 :if score < sch_ave_score - 15 :result = 0elif score < sch_ave_score - 5 :result = 1elif score < sch_ave_score + 5 :result = 2else :result = 3# 再根据最低分判断,会覆盖线上分结果if sch_min_score > 0 :if score < sch_min_score :result = 0elif score < sch_min_score + 10 :result = 1elif score < sch_min_score + 20:result = 2else :result = 3
recommend_school_rank(origin, type, score)
# 参数: origin:省份(不含"省"字,如"山东""新疆""西藏"), type:"文科"或 "理科", score:分数
# 返回值:((保底学校1,保底学校2...),(推荐学校1,推荐学校2...),(冲一冲学校1,冲一冲学校2...)); 学校信息包括(学校名,预测分数,批次)
此函数返回五个学校列表,每个学校是一个三元组(学校名称,平均分,批次)
- 太亏,预测最低分在考生分数-20以下
- 保底学校,预测最低分在考生分数-20到-10的学校
- 推荐学校,预测最低分在考生分数-10到0的学校
- 冲一冲学校,预测最低分在考生分数+0到+10的学校
- 不可能,预测最低分在考生分数+10以上
这五种情况的SQL语句都很相似,不同之处在于min_score分数不同。
select school,average_score,batch
from school_score
where year = 2016 and student_origin = "%s" and student_type = "%s" and min_score > %d + 10' % (origin, type, score)
recommend_school_answer(origin,type,score)
这个函数返回值是str类型的。这个函数主要用来进行单元测试。
再来看recommend_major.py
本模块有一个major.txt,里面是各个专业的名称缩写。
init_major_list()
初始化专业列表,将数据库中各个专业和major.txt中的缩写对应起来。
predict_major_fullname(origin, type, score,school,major)
给定省份、文理、分数、年份,判断能不能上某学校的某专业。
原理就是查询score_major表,得到该学校该专业的分数,根据分数差分为4个等级,来表达小强的态度。
参数:origin:省份(不含"省"字,如"山东""新疆""西藏"),type:"文科"或 "理科", score:分数, school:学校名, major:专业全称
返回值:{'result':0/1/2/3(基本不可能/可能性较小/有把握/太亏), 'major_score':预测平均分},各项若为-1则是缺少数据,数据不足以做出判断则返回None
predict_major(origin, type, score, school, major)
这个函数是上面predict_major_fullname()的包装,它首先获取专业简写对应的全部专业,然后调用predict_major_fullname()函数。
# 判断能不能上某学校专业
# 参数: origin:省份(不含"省"字,如"山东""新疆""西藏"), type:"文科"或 "理科", score:分数, school:学校名, major:专业简称
# 返回值:{专业全称:{'result':0/1/2/3(基本不可能/可能性较小/有把握/太亏), 'major_score':预测平均分}},每个全称对应一条结果,若结果为空则该学校无对应专业,各项若为-1则是缺少数据,数据不足以做出判断则返回None
def predict_major(origin, type, score, school, major) :r = {}if major in majors.keys() :major_full = majors[major]for m in major_full :r[m] = predict_major_fullname(origin,type,score,school,m)else :return Nonereturn r
recommend_school_fullname(origin, type, score, major)
根据省份、分数、专业推荐学校。
# 参数: origin:省份(不含"省"字,如"山东""新疆""西藏"), type:"文科"或 "理科", score:分数, major:专业全称
# 返回值:{保底学校:学校列表,推荐学校:学校列表,冲一冲学校:学校列表
}
实现就是SQL语句,三类学校分数差不同。这跟recommend_school.py中直接推荐学校很相似。
select school,average_score,batch from major_score where year = 2016 and student_origin = "%s" and student_type = "%s" and major = "%s" and average_score > %d + 5 and average_score < %d + 15' % (origin, type, major, score, score)
recommend_major(origin,type,score,major)
用户查询的major是简写的专业名称,一个专业简写对应多个全名专业。
根据专业推荐学校,分两步:
- 把简写的专业进行扩展,得到一个专业列表
- 对于专业列表中的每一个专业推荐学校
def recommend_major(origin,type_in,score,major) :r = {}if major in majors:major_full = majors[major]for m in major_full :r[m] = recommend_school_fullname(origin,type_in,score,m)else :return Nonereturn r
StateTracking模块:用户状态变化
相关文件
- _type.py:定义了一些枚举
- state.py:定义了状态变化
先看_type.py中的枚举,需要说明的是Python2实现枚举比较麻烦,Python3中枚举变得非常简单了。
def my_enum(**enums):return type('Enum', (), enums)StateType = my_enum(INIT = 0,ENOUGH_BASIC_INFO = 1,ASK_BACK_FOR_SOMETHING = 2,TO_INIT = 3,
)IntentType = my_enum(NORMAL_INQUIRE = 1,HOW_ABOUT = 2,ASK_FUNCTION = 4,)AskType = my_enum(NONE = 0,ORIGIN = 1,TYPE = 2,SCORE = 3,AMBIGUOUS = 4,
)
这三个枚举非常重要,是理解整个系统的重要入口。
用户状态枚举:开始、足够信息、信息不全
用户意图枚举:正常询问、怎么样(“我能上清华大学吗”)、询问功能(“这个系统怎么用啊”)
用户信息枚举:用来记录用户当前提供了哪些信息,包括什么信息也没有、有了省份信息、有了文理信息、有了分数信息等。
state.py定义的是上下文信息。
首先定义了class Info,它有两个成员tag,info。其实就是键值对。
然后定义了AmbiguousInfo,它有一个TTL类型的成员变量,表示如果问你两次你都没回答就不搭理你了。
最后定义了核心类State,这个类包含了从用户查询中提取出来的全部信息。
State类维护了两个Info列表:
keyInfoList
otherInfoList
还定义了一个tag列表:
keyInfoTags
实际上State类就相当于一个字典,里面存放的就是键值对。可以通过对比keyInfoTags和keyInfoList找出缺少的键值。
State就是用来存储解析出来的:分数、省份、文理等信息的。
InfomationRecognition模块:信息识别模块
这个模块在Dict文件夹中定义了几个同义词列表,每一个文件中的内容都是同义词。
- agree.txt:好、行、恩、可以、没问题
- asktone.txt:行不行、能不能、是不是、算不算
- disagree.txt:不、别、否
- gongneng.txt:什么功能、能做什么、能干什么
- howabout.txt:怎么样、介绍
- normalinquire.txt:能上、能报、可以上、可以报
if_agree.py
if_agree.py定义了一个AgreeJudge类,这个类读取agree.txt和disagree.txt中的词语构建同意和不同意两个字典。
判断同意还是不同意时,直接判断query中是否包含“同意字典”中的词语。同意返回1,不同意返回-1,不确定返回0。
def judge(self, target):# 先判定是否有否定内容,再判断是否有肯定内容for pattern in self.__disagree_pattern:index = target.find(pattern)if index == -1:passelse:return -1for pattern in self.__agree_pattern:index = target.find(pattern)if index == -1:passelse:return 1return 0
major.py:识别用户查询中的专业
major.py用来识别出用户查询语句中的专业信息。
首先定义一个专业字典['历史','语言','中医','中药'......]
,search()函数定义如下,如果用户查询中包含专业,则返回专业名称。如果不包含,返回None
def search(self, target):for word in self.__all_major:index = target.find(word)if index == -1:passelse:return wordreturn None
target.py:识别出用户查询中的省份
此文件定义了10个省份列表:
- 北方、南方
- 华东、华北、华中、华南
- 西北、东北、西南
- 全国
每一个列表都形如['河北','河南','北京'......]
_label_to_list(label)函数将地区名称映射为省份列表。
determin_area、determin_province函数分别用来确定学校是否属于某个地区、学校是否属于某个省份
search_province(query):返回query中包含的省份名称,如果没有返回None
ac_auto.py
此文件实现了一个AC自动机,它读取Dict目录下的normal_inquire、how_about、ask_tone、gongneng四个词典中的词语构建一棵字典树。
infomation_extraction:信息抽取模块
这一部分代码是我最看不懂的代码,也是离自然语言处理最近的代码。
本模块用到了
- 哈工大分词器LTP,命名实体识别
- jieba分词器
- ahocorasick,即AC自动机,python中有AC自动机的包
在_types.py文件中定义了需要提取到的信息的枚举
from enum import Enumclass Attribute(Enum):ORIGIN = 0TYPE = 1SCORE = 2DESTINATION = 3SCHOOL = 4
根包下的文件
process.py是最重要的文件,它集中调用上面各个模块。
XQGKFlask是微信接口,本系统调用了wechatpy包。