读别人的代码像破解密码一样,乐趣无穷。
高考小强是一个基于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 = 0
elif score < sch_ave_score - 5 :
result = 1
elif score < sch_ave_score + 5 :
result = 2
else :
result = 3
# 再根据最低分判断,会覆盖线上分结果
if sch_min_score > 0 :
if score < sch_min_score :
result = 0
elif score < sch_min_score + 10 :
result = 1
elif score < sch_min_score + 20:
result = 2
else :
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:学校名, m
ajor:专业全称
返回值:{'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 None
return 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 None
return 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:
pass
else:
return -1
for pattern in self.__agree_pattern:
index = target.find(pattern)
if index == -1:
pass
else:
return 1
return 0
major.py:识别用户查询中的专业
major.py用来识别出用户查询语句中的专业信息。
首先定义一个专业字典['历史','语言','中医','中药'......]
,search()函数定义如下,如果用户查询中包含专业,则返回专业名称。如果不包含,返回None
def search(self, target):
for word in self.__all_major:
index = target.find(word)
if index == -1:
pass
else:
return word
return 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 Enum
class Attribute(Enum):
ORIGIN = 0
TYPE = 1
SCORE = 2
DESTINATION = 3
SCHOOL = 4
根包下的文件
process.py是最重要的文件,它集中调用上面各个模块。
XQGKFlask是微信接口,本系统调用了wechatpy包。