NLPCamp-Project1

2 篇文章 0 订阅

Project1


Part 1: 搭建一个分词工具

Part 1.1 基于枚举方法来搭建中文分词工具

此项目需要的数据:

  1. 综合类中文词库.xlsx: 包含了中文词,当做词典来用
  2. 以变量的方式提供了部分unigram概率 word_prob

举个例子: 给定词典=[我们 学习 人工 智能 人工智能 未来 是], 另外我们给定unigram概率:p(我们)=0.25, p(学习)=0.15, p(人工)=0.05, p(智能)=0.1, p(人工智能)=0.2, p(未来)=0.1, p(是)=0.15

Step 1: 对于给定字符串:”我们学习人工智能,人工智能是未来“, 找出所有可能的分割方式
  • [我们,学习,人工智能,人工智能,是,未来]
  • [我们,学习,人工,智能,人工智能,是,未来]
  • [我们,学习,人工,智能,人工,智能,是,未来]
  • [我们,学习,人工智能,人工,智能,是,未来]
Step 2: 我们也可以计算出每一个切分之后句子的概率
  • p(我们,学习,人工智能,人工智能,是,未来)= -log p(我们)-log p(学习)-log p(人工智能)-log p(人工智能)-log p(是)-log p(未来)
  • p(我们,学习,人工,智能,人工智能,是,未来)=-log p(我们)-log p(学习)-log p(人工)-log p(智能)-log p(人工智能)-log p(是)-log p(未来)
  • p(我们,学习,人工,智能,人工,智能,是,未来)=-log p(我们)-log p(学习)-log p(人工)-log p(智能)-log p(人工)-log p(智能)-log p(是)-log p(未来)
  • p(我们,学习,人工智能,人工,智能,是,未来)=-log p(我们)-log p(学习)-log p(人工智能)-log p(人工)-log p(智能)-log(是)-log p(未来)
Step 3: 返回第二步中概率最大的结果
# TODO: 第一步: 从dic.txt中读取所有中文词。
#  hint: 思考一下用什么数据结构来存储这个词典会比较好? 要考虑我们每次查询一个单词的效率。 

# EDIT START
# 读取xlsx
import xlrd
import os
data = xlrd.open_workbook(os.path.join('data', '综合类中文词库.xlsx'))
table = data.sheets()[0]
dic_words = {table.row_values(row_index)[0]: 0.00001 for row_index in range(table.nrows)}   # 保存词典库中读取的单词
# EDIT END

# 以下是每一个单词出现的概率。为了问题的简化,我们只列出了一小部分单词的概率。 在这里没有出现的的单词但是出现在词典里的,统一把概率设置成为0.00001
# 比如 p("学院")=p("概率")=...0.00001

word_prob = {"北京":0.03,"的":0.08,"天":0.005,"气":0.005,"天气":0.06,"真":0.04,"好":0.05,"真好":0.04,"啊":0.01,"真好啊":0.02, 
             "今":0.01,"今天":0.07,"课程":0.06,"内容":0.06,"有":0.05,"很":0.03,"很有":0.04,"意思":0.06,"有意思":0.005,"课":0.01,
             "程":0.005,"经常":0.08,"意见":0.08,"意":0.01,"见":0.005,"有意见":0.02,"分歧":0.04,"分":0.02, "歧":0.005}

print (sum(word_prob.values()))

# EDIT START
dic_words.update(word_prob)
word_prob = dic_words
dic_words = set(dic_words.keys())
# EDIT END
1.0000000000000002
#  分数(10)
## TODO 请编写word_segment_naive函数来实现对输入字符串的分词
def word_segment_naive(input_str):
    """
    1. 对于输入字符串做分词,并返回所有可行的分词之后的结果。
    2. 针对于每一个返回结果,计算句子的概率
    3. 返回概率最高的最作为最后结果
    
    input_str: 输入字符串   输入格式:“今天天气好”
    best_segment: 最好的分词结果  输出格式:["今天","天气","好"]
    """

    # TODO: 第一步: 计算所有可能的分词结果,要保证每个分完的词存在于词典里,这个结果有可能会非常多。 
    segments = []  # 存储所有分词的结果。如果次字符串不可能被完全切分,则返回空列表(list)
                   # 格式为:segments = [["今天",“天气”,“好”],["今天",“天“,”气”,“好”],["今“,”天",“天气”,“好”],...]
    
    # EDIT START
    # 非递归实现
    stack = [(input_str, [])]  # 栈
    while len(stack) != 0:
        i_str, seg = stack.pop()
        if i_str in dic_words:
            segments.append(seg + [i_str])
        for index in range(1, len(i_str)):
            if i_str[:index] in dic_words:
                stack.append((i_str[index:], seg + [i_str[:index]]))
    # 递归实现
#     def segment(i_str, seg):
#         if i_str in dic_words:
#             segments.append(seg + [i_str])
#         for index in range(1, len(i_str)):
#             if i_str[:index] in dic_words:
#                 segment(i_str[index:], seg + [i_str[:index]])
#     segment(input_str, [])
    # EDIT END
    
    # TODO: 第二步:循环所有的分词结果,并计算出概率最高的分词结果,并返回
    # EDIT START
    from math import log
    best_segment = None
    best_score = float('inf')
    for seg in segments:
        # TODO ...
        score = sum([-log(word_prob[word]) for word in seg])
        if best_score > score:
            best_score = score
            best_segment = seg
    # EDIT END
    return best_segment      
# 测试
print (word_segment_naive("北京的天气真好啊"))
print (word_segment_naive("今天的课程内容很有意思"))
print (word_segment_naive("经常有意见分歧"))
['北京', '的', '天气', '真好啊']
['今天', '的', '课程', '内容', '很有', '意思']
['经常', '有意见', '分歧']

Part 1.2 基于维特比算法来优化上述流程

此项目需要的数据:

  1. 综合类中文词库.xlsx: 包含了中文词,当做词典来用
  2. 以变量的方式提供了部分unigram概率word_prob

举个例子: 给定词典=[我们 学习 人工 智能 人工智能 未来 是], 另外我们给定unigram概率:p(我们)=0.25, p(学习)=0.15, p(人工)=0.05, p(智能)=0.1, p(人工智能)=0.2, p(未来)=0.1, p(是)=0.15

Step 1: 根据词典,输入的句子和 word_prob来创建带权重的有向图(Directed Graph) 参考:课程内容

有向图的每一条边是一个单词的概率(只要存在于词典里的都可以作为一个合法的单词),这些概率已经给出(存放在word_prob)。
注意:思考用什么方式来存储这种有向图比较合适? 不一定只有一种方式来存储这种结构。

Step 2: 编写维特比算法(viterebi)算法来找出其中最好的PATH, 也就是最好的句子切分

具体算法参考课程中讲过的内容

Step 3: 返回结果

跟PART 1.1的要求一致

# 分数(10)

## TODO 请编写word_segment_viterbi函数来实现对输入字符串的分词
def word_segment_viterbi(input_str):
    """
    1. 基于输入字符串,词典,以及给定的unigram概率来创建DAG(有向图)。
    2. 编写维特比算法来寻找最优的PATH
    3. 返回分词结果
    
    input_str: 输入字符串   输入格式:“今天天气好”
    best_segment: 最好的分词结果  输出格式:["今天","天气","好"]
    """
    
    # TODO: 第一步:根据词典,输入的句子,以及给定的unigram概率来创建带权重的有向图(Directed Graph) 参考:课程内容
    #      有向图的每一条边是一个单词的概率(只要存在于词典里的都可以作为一个合法的单词),这些概率在 word_prob,如果不在word_prob里的单词但在
    #      词典里存在的,统一用概率值0.00001。
    #      注意:思考用什么方式来存储这种有向图比较合适? 不一定有只有一种方式来存储这种结构。 
    
    # EDIT START
    # 针对单字不存在于字典中的,同样默认概率值0.00001
    import numpy as np
    # 假设graph矩阵中图方向为行指向列,仅使用上半三角矩阵(1不可能指向0),忽略自己指向自己
    input_str = input_str + " "  # 稍微处理一下输入字符串,能简化后面代码的参数
    matrix_size = len(input_str)
    graph = np.ones(shape=(matrix_size, matrix_size)) * float('inf')  # 初始化为无穷大,实际可以省略最后一行
    # 初始化有向图权重
    from math import log
    for i in range(matrix_size):
        for j in range(i + 1, matrix_size):
            if input_str[i:j] in dic_words:
                graph[i][j] = -log(word_prob[input_str[i:j]])
            elif j - i == 1:  # 当为单字的时候
                graph[i][j] = -log(0.00001)  # 默认单字概率0.00001
    # EDIT END
    
    # TODO: 第二步: 利用维特比算法来找出最好的PATH, 这个PATH是P(sentence)最大或者 -log P(sentence)最小的PATH。
    #              hint: 思考为什么不用相乘: p(w1)p(w2)...而是使用negative log sum:  -log(w1)-log(w2)-...
    
    # EDIT START
    f = [float('inf')] * matrix_size
    pt = [None] * matrix_size
    f[0] = pt[0] = 0
    for i in range(1, matrix_size):
        incoming_links = [f[index] + number for index, number in enumerate(graph[:, i])]  # inf+number仍是inf
        min_index = np.argmin(incoming_links)
        f[i] = np.min(incoming_links) + f[min_index]
        pt[i] = min_index
    # EDIT END
    
    # TODO: 第三步: 根据最好的PATH, 返回最好的切分
    
    # EDIT START
    best_segment = []
    front_index = back_index = matrix_size - 1  # 使用一个变量也可以,使用两个变量代码更易于理解
    while front_index != 0:
        front_index = pt[back_index]
        best_segment.append(input_str[front_index:back_index])
        back_index = front_index
    best_segment.reverse()
    # EDIT END
    
    return best_segment      
# 测试
# EDIT START
# 原始代码调用了前面的naive方法,修改为调用viterbi方法
print (word_segment_viterbi("北京的天气真好啊"))
print (word_segment_viterbi("今天的课程内容很有意思"))
print (word_segment_viterbi("经常有意见分歧"))
# EDIT END
['北京', '的', '天气', '真好啊']
['今天', '的', '课程', '内容', '很有', '意思']
['经常', '有意见', '分歧']
# 分数(3)
# TODO: 第一种方法和第二种方法的时间复杂度和空间复杂度分别是多少?
第一个方法: 
时间复杂度=O(M^N) , 空间复杂度=O(N)

第二个方法:
时间复杂度=O(MN) , 空间复杂度=O(N)  # 只考虑其中viterbi算法部分,忽略了图和最后获得切分
复杂度计算解释
naive时间复杂度

naive时间复杂度

viterbi时间复杂度

viterbi时间复杂度


Part 2: 搭建一个简单的问答系统

本次项目的目标是搭建一个基于检索式的简单的问答系统。至于什么是检索式的问答系统请参考课程直播内容/PPT介绍。

通过此项目,你将会有机会掌握以下几个知识点:

  1. 字符串操作 2. 文本预处理技术(词过滤,标准化) 3. 文本的表示(tf-idf, word2vec) 4. 文本相似度计算 5. 文本高效检索

此项目需要的数据:

  1. dev-v2.0.json: 这个数据包含了问题和答案的pair, 但是以JSON格式存在,需要编写parser来提取出里面的问题和答案。
  2. glove.6B: 这个文件需要从网上下载,下载地址为:https://nlp.stanford.edu/projects/glove/, 请使用d=100的词向量
检索式的问答系统

问答系统所需要的数据已经提供,对于每一个问题都可以找得到相应的答案,所以可以理解为每一个样本数据是 <问题、答案>。 那系统的核心是当用户输入一个问题的时候,首先要找到跟这个问题最相近的已经存储在库里的问题,然后直接返回相应的答案即可。 举一个简单的例子:

假设我们的库里面已有存在以下几个<问题,答案>:
<"贪心学院主要做什么方面的业务?”, “他们主要做人工智能方面的教育”>
<“国内有哪些做人工智能教育的公司?”, “贪心学院”>
<“人工智能和机器学习的关系什么?”, “其实机器学习是人工智能的一个范畴,很多人工智能的应用要基于机器学习的技术”>
<“人工智能最核心的语言是什么?”, ”Python“>

假设一个用户往系统中输入了问题 “贪心学院是做什么的?”, 那这时候系统先去匹配最相近的“已经存在库里的”问题。 那在这里很显然是 “贪心学院是做什么的”和“贪心学院主要做什么方面的业务?”是最相近的。 所以当我们定位到这个问题之后,直接返回它的答案 “他们主要做人工智能方面的教育”就可以了。 所以这里的核心问题可以归结为计算两个问句(query)之间的相似度。

在本次项目中,你会频繁地使用到sklearn这个机器学习库。具体安装请见:http://scikit-learn.org/stable/install.html sklearn包含了各类机器学习算法和数据处理工具,包括本项目需要使用的词袋模型,均可以在sklearn工具包中找得到。 另外,本项目还需要用到分词工具jieba, 具体使用方法请见 https://github.com/fxsjy/jieba

Part 2.1 第一部分: 读取文件,并把内容分别写到两个list里(一个list对应问题集,另一个list对应答案集)

# 分数(5)
def read_corpus():
    """
    读取给定的语料库,并把问题列表和答案列表分别写入到 qlist, alist 里面。 在此过程中,不用对字符换做任何的处理(这部分需要在 Part 2.3里处理)
    qlist = ["问题1", “问题2”, “问题3” ....]
    alist = ["答案1", "答案2", "答案3" ....]
    务必要让每一个问题和答案对应起来(下标位置一致)
    """
    # EDIT START
    # 数据中未找到dev-v2.0.json,故使用train-v2.0.json
    # 读取数据
    import json
    import os
    with open(os.path.join('data', 'train-v2.0.json'), 'r', encoding='utf-8') as f:
        paragraphs = [p['paragraphs'] for p in json.load(f)['data']]
    qlist = []
    alist = []
    for p in paragraphs:
        for i in p:
            for qa in i['qas']:
                for a in qa['answers']:
                    qlist.append(qa['question'])
                    alist.append(a['text'])
    # 数据处理(执行Part2.2时需注释该部分)
    # TODO: 对于qlist, alist做文本预处理操作。 可以考虑以下几种操作:
    # 仅处理qlist
    # 2. 转换成lower_case: 这是一个基本的操作  
    qlist = [q.lower() for q in qlist]
    # 3. 去掉一些无用的符号: 比如连续的感叹号!!!, 或者一些奇怪的单词。
    import re
    import unicodedata
    qlist = [str(unicodedata.normalize('NFKD', q).encode('ascii', 'ignore'), encoding='utf-8') for q in qlist]  # 去除音调
    number_placeholder = "#number"
    start_symbol = number_placeholder[0]
    patterns = [
        (r'won\'t', 'will not'),  # 替换won't
        (r'can\'t', 'cannot'),  # 替换can't
        (r'i\'m', 'i am'),  # 替换i'm
        (r'ain\'t', 'is not'),  # 替换ain't
        (r'(\w+)\'ll', '\g<1> will'),  # 替换'll
        (r'(\w+)n\'t', '\g<1> not'),  # 替换n't
        (r'(\w+)\'ve', '\g<1> have'),  # 替换've
        (r'(\w+)\'s', '\g<1> is'),  # 替换's,实际变为is也在stopwords中
        (r'(\w+)\'re', '\g<1> are'),  # 替换're
        (r'(\w+)\'d', '\g<1> would'),  # 替换'd
        (r'%s' % start_symbol, ' '),  # 去除#
        (r'[\.|,]', ''),  # 去除,.
        (r'\s&|\-\s', ' '),  # 去除两边均有空格的&-(词内部包含&-的保留,例at&t)
        (r'\s[a-z]*[0-9]*\-?[0-9]+[a-z]*\s', " %s " % number_placeholder),  # 5. 对于数字的处理: 分词完只有有些单词可能就是数字比如44,415,把所有这些数字都看成是一个单词,这个新的单词我们可以定义为 "#number"
        (r'[^a-z0-9|\-|&|\s|%s]' % start_symbol, ' '),  # 去掉非英文字符
    ]
    patterns = [(re.compile(regex), repl) for regex, repl in patterns]
    for i, q in enumerate(qlist):
        for pattern, repl in patterns:
            q = re.sub(pattern, repl, q)
        qlist[i] = q.split()  # 分词
    # 3、5、分词可以分开(格外遍历一次),也可以合并在一起(速度会稍微快一些)
    
    # 1. 停用词过滤 (去网上搜一下 "english stop words list",会出现很多包含停用词库的网页,或者直接使用NLTK自带的)   
    from nltk.corpus import stopwords
    stopwords_set = set(stopwords.words('english'))
    qlist = [[word for word in q if word not in stopwords_set] for q in qlist]
    
    # 4. 去掉出现频率很低的词:比如出现次数少于10,20....
    threshold = 10
    from collections import Counter
    counter = Counter()
    _ = [counter.update(q) for q in qlist]
    ignore_words = {word for word, count in counter.items() if count < threshold}
    qlist = [[word for word in q if word not in ignore_words] for q in qlist]

    # 6. stemming(利用porter stemming): 因为是英文,所以stemming也是可以做的工作
    from nltk.stem.porter import PorterStemmer
    ps = PorterStemmer()
    qlist = [[ps.stem(word) if word != number_placeholder else word for word in q] for q in qlist]
    
    # 分词合并成一句
    qlist = [" ".join(q) for q in qlist]
    # EDIT END

    assert len(qlist) == len(alist)  # 确保长度一样
    # EDIT START
    for i in range(len(qlist) - 1, -1, -1):
        if not len(qlist[i]) or not len(alist[i]) or qlist[i] == number_placeholder or qlist[i] == 'midna':
            del qlist[i]
            del alist[i]
    # 若不进行排查,使用词向量表示时句子向量为nan
    import numpy as np
    alist = np.array(alist)
    # EDIT END
    return qlist, alist  # alist转换为np.array类型便于2.5部分top5results使用

Part 2.2 理解数据(可视化分析/统计信息)

对数据的理解是任何AI工作的第一步,需要充分对手上的数据有个更直观的理解。

# 分数(10)
# TODO: 统计一下在qlist 总共出现了多少个单词? 总共出现了多少个不同的单词?
#       这里需要做简单的分词,对于英文我们根据空格来分词即可,其他过滤暂不考虑(只需分词)

# EDIT START
qlist, alist = read_corpus()
word_total = []
for q in qlist:
    word_total += q.split()
# EDIT END

# EDIT START
# 原代码直接打印word_total,由于notebook输出限制实际无法打印,故修改
print ("总共出现了%d个单词" % len(word_total))
print ("总共出现了%d个不同的单词" % len(set(word_total)))
# EDIT END
总共出现了873973个单词
总共出现了63779个不同的单词
# TODO: 统计一下qlist中每个单词出现的频率,并把这些频率排一下序,然后画成plot. 比如总共出现了总共7个不同单词,而且每个单词出现的频率为 4, 5,10,2, 1, 1,1
#       把频率排序之后就可以得到(从大到小) 10, 5, 4, 2, 1, 1, 1. 然后把这7个数plot即可(从大到小)
#       需要使用matplotlib里的plot函数。y轴是词频

# EDIT START
# 根据理解,可能是让绘制:x:词的数量,y:词频(幂律)
x = []
y = []
# 使用Counter进行计数(耗时0.09s)
from collections import Counter
word_count = [count for _, count in Counter(word_total).items()]

# 直接进行计数(耗时12min)
# from tqdm import tqdm  # 实际运行时间较长,可能误以为代码出错,故使用进度条来观察计算进度
# word_count = [word_total.count(word) for word in tqdm(set(word_total))]

y = list(set(word_count))
y.sort()
_ = [x.append(word_count.count(count)) for count in y]
import matplotlib.pyplot as plt
plt.plot(x, y, '.')
plt.show()
# EDIT END

plt.show()

# TODO: 从上面的图中能观察到什么样的现象? 这样的一个图的形状跟一个非常著名的函数形状很类似,能所出此定理吗? 
#       hint: [XXX]'s law
# 
# 

# EDIT START
# 幂律,Power laws
# EDIT END
# TODO: 在qlist和alist里出现次数最多的TOP 10单词分别是什么? 

# EDIT START
top_k = 10
from collections import Counter
alist_words = []
for a in alist:
    alist_words += a.split()
qlist_words = word_total
# 计算top10
print("qlist里出现次数最多的TOP 10")
print("  %s" % " ".join([word for word, _ in Counter(qlist_words).most_common(top_k)]))
print("alist里出现次数最多的TOP 10")
print("  %s" % " ".join([word for word, _ in Counter(alist_words).most_common(top_k)]))
# EDIT END
qlist里出现次数最多的TOP 10
  the What of in to was is did what a
alist里出现次数最多的TOP 10
  the of and to a in The or for million

2.3 文本预处理

次部分需要尝试做文本的处理。在这里我们面对的是英文文本,所以任何对英文适合的技术都可以考虑进来。

# 分数(10)

# TODO: 对于qlist, alist做文本预处理操作。 可以考虑以下几种操作:
#       1. 停用词过滤 (去网上搜一下 "english stop words list",会出现很多包含停用词库的网页,或者直接使用NLTK自带的)   
#       2. 转换成lower_case: 这是一个基本的操作   
#       3. 去掉一些无用的符号: 比如连续的感叹号!!!, 或者一些奇怪的单词。
#       4. 去掉出现频率很低的词:比如出现次数少于10,20....
#       5. 对于数字的处理: 分词完只有有些单词可能就是数字比如44,415,把所有这些数字都看成是一个单词,这个新的单词我们可以定义为 "#number"
#       6. stemming(利用porter stemming): 因为是英文,所以stemming也是可以做的工作
#       7. 其他(如果有的话)
#       请注意,不一定要按照上面的顺序来处理,具体处理的顺序思考一下,然后选择一个合理的顺序
#  hint: 停用词用什么数据结构来存储? 不一样的数据结构会带来完全不一样的效率! 

# EDIT START
qlist, alist = read_corpus()   # 更新后的
# EDIT END
# TODO: 在前面步骤里,我们删除了出现次数比较少的单词,那你选择的阈值是多少(小于多少的去掉?), 这个阈值是根据什么来选择的? 
# 

# EDIT START
# 观察绘制的图像与总的单词数和不同的单词数的比例,多次尝试不同阈值
# EDIT END

2.4 文本表示

当我们做完关键的预处理过程之后,就需要把每一个文本转换成向量。

# 分数(10)

# TODO: 把qlist中的每一个问题字符串转换成tf-idf向量, 转换之后的结果存储在X矩阵里。 X的大小是: N* D的矩阵。 这里N是问题的个数(样本个数),
#       D是字典库的大小。 

# EDIT START
from sklearn.feature_extraction.text import TfidfVectorizer
vectorizer = TfidfVectorizer(token_pattern=r'(?u)\S+\b') # 定义一个tf-idf的vectorizer

X = vectorizer.fit_transform(qlist).toarray()  # 结果存放在X矩阵
# 词典大小与X列数是否相等
from collections import Counter
counter = Counter()
_ = [counter.update(q.split()) for q in qlist]
assert_str = '|词典| != X列数\n原因可能\n1.read_corpus内正则\n2.TfidfVectorizer中token_pattern参数'
assert len(counter) == len(X[0]), assert_str
# EDIT END
# TODO: 矩阵X有什么特点? 计算一下它的稀疏度

# EDIT START
import numpy as np
sparsity = 1 - np.count_nonzero(X) / (len(X) * len(X[0]))
print (sparsity)  # 打印出稀疏度(sparsity)
# EDIT END
0.9989155003883418

2.5 对于用户的输入问题,找到相似度最高的TOP5问题,并把5个潜在的答案做返回

# 分数(10)
# EDIT START
# 3. 去掉一些无用的符号: 比如连续的感叹号!!!, 或者一些奇怪的单词。
from nltk.corpus import stopwords
from nltk.stem.porter import PorterStemmer
from queue import PriorityQueue
import re
import numpy as np
import unicodedata
number_placeholder = "#number"
start_symbol = number_placeholder[0]
patterns = [
    (r'won\'t', 'will not'),  # 替换won't
    (r'can\'t', 'cannot'),  # 替换can't
    (r'i\'m', 'i am'),  # 替换i'm
    (r'ain\'t', 'is not'),  # 替换ain't
    (r'(\w+)\'ll', '\g<1> will'),  # 替换'll
    (r'(\w+)n\'t', '\g<1> not'),  # 替换n't
    (r'(\w+)\'ve', '\g<1> have'),  # 替换've
    (r'(\w+)\'s', '\g<1> is'),  # 替换's,实际变为is也在stopwords中
    (r'(\w+)\'re', '\g<1> are'),  # 替换're
    (r'(\w+)\'d', '\g<1> would'),  # 替换'd
    (r'%s' % start_symbol, ' '),  # 去除#
    (r'[\.|,]', ''),  # 去除,.
    (r'\s&|\-\s', ' '),  # 去除两边均有空格的&-(词内部包含&-的保留,例at&t)
    (r'\s[a-z]*[0-9]*\-?[0-9]+[a-z]*\s', " %s " % number_placeholder),  # 5. 对于数字的处理: 分词完只有有些单词可能就是数字比如44,415,把所有这些数字都看成是一个单词,这个新的单词我们可以定义为 "#number"
    (r'[^a-z0-9|\-|&|\s|%s]' % start_symbol, ' '),  # 去掉非英文字符
]
patterns = [(re.compile(regex), repl) for regex, repl in patterns]
# 1. 停用词过滤 (去网上搜一下 "english stop words list",会出现很多包含停用词库的网页,或者直接使用NLTK自带的)   
stopwords_set = set(stopwords.words('english'))
# 6. stemming(利用porter stemming): 因为是英文,所以stemming也是可以做的工作
ps = PorterStemmer()
def preprocess(input_q):
    # input_q预处理
    # 2. 转换成lower_case: 这是一个基本的操作  
    input_q = input_q.lower()
    input_q = str(unicodedata.normalize('NFKD', input_q).encode('ascii', 'ignore'), encoding='utf-8')  # 去除音调
    for pattern, repl in patterns:
        input_q = re.sub(pattern, repl, input_q)
    input_q = input_q.split()  # 分词
    # 不需要去除停用词和小于阈值的词(计算tfidf向量时只会依据已有词典生成,词典中不包含则生成的向量也不会包含)
    input_q = [ps.stem(word) if word != number_placeholder else word for word in input_q]
    input_q = " ".join(input_q)
    return input_q
# EDIT END
 
def top5results(input_q):
    """
    给定用户输入的问题 input_q, 返回最有可能的TOP 5问题。这里面需要做到以下几点:
    1. 对于用户的输入 input_q 首先做一系列的预处理,然后再转换成tf-idf向量(利用上面的vectorizer)
    2. 计算跟每个库里的问题之间的相似度
    3. 找出相似度最高的top5问题的答案
    """
    # EDIT START
    top_idxs = []  # top_idxs存放相似度最高的(存在qlist里的)问题的下表 
                   # hint: 利用priority queue来找出top results. 思考为什么可以这么做?
    input_q = preprocess(input_q)
    # 生成input_q的tfidf向量
    top_k = 5
    input_q_vec = vectorizer.transform([input_q]).toarray()[0]
    # 计算相似度
    if np.linalg.norm(input_q_vec) == 0:
        return []
    cosine_similarity = np.sum(np.multiply(X, input_q_vec), axis=1) / (np.linalg.norm(X, axis=1) * np.linalg.norm(input_q_vec))
    # 使用PriorityQueue
    que = PriorityQueue()
    _ = [que.put((-cosine, index)) for index, cosine in enumerate(cosine_similarity)]
    for _ in range(top_k):
        top_idxs.append(que.get()[1])
    # EDIT END
    return alist[top_idxs]  # 返回相似度最高的问题对应的答案,作为TOP5答案    
# TODO: 编写几个测试用例,并输出结果
# EDIT START
# 原问题:How many records has Beyonce sold in her 19 year career? 答案:118 million
print (top5results("How many records has Beyoncé sold?"))
# 原问题:Beyonce became the highest-paid black musical in which year? 答案:2014
print (top5results("Which year did Beyonce become the highest-paid black musical?"))
# 微调阈值可能会对结果产生影响
# EDIT END
['over 15 million' '118 million' 'over 118 million' '118 million'
 '14 million']
['R&B' '2017' '2009' '2014' 'Africa']
# 分数(5)

# TODO: 上面的top5results算法的时间复杂度和空间复杂度分别是多少?

# EDIT STATR
时间复杂度 = O(V) * O_cosine, 空间复杂度 = O(V)  # |词典| = V
# EDIT END

2.6 利用倒排表的优化。

上面的算法,一个最大的缺点是每一个用户问题都需要跟库里的所有的问题都计算相似度。假设我们库里的问题非常多,这将是效率非常低的方法。 这里面一个方案是通过倒排表的方式,先从库里面找到跟当前的输入类似的问题描述。然后针对于这些candidates问题再做余弦相似度的计算。这样会节省大量的时间。

# 分数(10)

# TODO: 基于倒排表的优化。在这里,我们可以定义一个类似于hash_map, 比如 inverted_index = {}, 然后存放包含每一个关键词的文档出现在了什么位置,
#       也就是,通过关键词的搜索首先来判断包含这些关键词的文档(比如出现至少一个),然后对于candidates问题做相似度比较。
# 
# EDIT START
inverted_idx = {}  # 定义一个简单的倒排表
split_qlist = [q.split() for q in qlist]
from collections import Counter
counter = Counter()
_ = [counter.update(q.split()) for q in qlist]
inverted_idx = {word: set() for word in counter}
_ = [[inverted_idx[word].add(index) for word in split_q] for index, split_q in enumerate(split_qlist)]
del split_qlist
# EDIT END
def top5results_invidx(input_q):
    """
    给定用户输入的问题 input_q, 返回最有可能的TOP 5问题。这里面需要做到以下几点:
    1. 利用倒排表来筛选 candidate
    2. 对于用户的输入 input_q 首先做一系列的预处理,然后再转换成tf-idf向量(利用上面的vectorizer)
    3. 计算跟每个库里的问题之间的相似度
    4. 找出相似度最高的top5问题的答案
    """
    # EDIT START
    input_q = preprocess(input_q)
    top_k = 5
    top_idxs = []
    # 筛选
    idx = set()
    _ = [idx.update(inverted_idx[word]) for word in input_q.split() if word in inverted_idx]
    idx = list(idx)
    # 生成input_q的tfidf向量
    input_q_vec = vectorizer.transform([input_q]).toarray()[0]
    # 计算相似度
    if np.linalg.norm(input_q_vec) == 0 or len(idx) == 0:
        return []
    candidate_X = X[idx]
    cosine_similarity = np.sum(np.multiply(candidate_X, input_q_vec), axis=1) / (np.linalg.norm(candidate_X, axis=1) * np.linalg.norm(input_q_vec))
    # 使用PriorityQueue
    que = PriorityQueue()
    _ = [que.put((-cosine, idx[index])) for index, cosine in enumerate(cosine_similarity)]
    for _ in range(top_k):
        top_idxs.append(que.get()[1])
    return alist[top_idxs]  # 返回相似度最高的问题对应的答案,作为TOP5答案    
    # EDIT END
# TODO: 编写几个测试用例,并输出结果
# EDIT START
# 原问题:How many records has Beyonce sold in her 19 year career? 答案:118 million
print (top5results_invidx("How many records has Beyoncé sold?"))
# 原问题:Beyonce became the highest-paid black musical in which year? 答案:2014
print (top5results_invidx("Which year did Beyonce become the highest-paid black musical?"))
# 微调阈值可能会对结果产生影响
# EDIT END
['over 15 million' '118 million' 'over 118 million' '118 million'
 '14 million']
['R&B' '2017' '2009' '2014' 'Africa']
# 分数(3)

# TODO: 上面的top5results算法的时间复杂度和空间复杂度分别是多少?

# EDIT START
时间复杂度 = O(V_) * O_cosine, 空间复杂度 = O(V_)  # |筛选后集合| = V_ << V
# EDIT END

2.7 基于词向量的文本表示

上面所用到的方法论是基于词袋模型(bag-of-words model)。这样的方法论有两个主要的问题:1. 无法计算词语之间的相似度 2. 稀疏度很高。 在2.7里面我们
讲采用词向量作为文本的表示。词向量方面需要下载: https://nlp.stanford.edu/projects/glove/ (请下载glove.6B.zip),并使用d=100的词向量(100维)。

# 分数(10)

# TODO
# EDIT START
import os
import numpy as np
with open(os.path.join('data', 'glove.6B.100d.txt'), 'r', encoding='utf-8') as f:
    glove = f.readlines()
    glove = [line.strip().split() for line in glove]
emb = {}# 读取每一个单词的嵌入。这个是 D*H的矩阵,这里的D是词典库的大小, H是词向量的大小。 这里面我们给定的每个单词的词向量,那句子向量怎么表达?
      # 其中,最简单的方式 句子向量 = 词向量的平均(出现在问句里的), 如果给定的词没有出现在词典库里,则忽略掉这个词。
from nltk.stem.porter import PorterStemmer
ps = PorterStemmer()
emb = {ps.stem(word): [float(p) for p in vec] for word, *vec in glove if ps.stem(word) in counter}
del glove
# 直接修改X
X = np.array([np.average([emb[word] for word in q.split() if word in emb], axis=0) for q in qlist])
# 由于使用PorterStemmer,不能保证词干为合法词汇,可能不存在于glove中
# EDIT END

def top5results_emb(input_q):
    """
    给定用户输入的问题 input_q, 返回最有可能的TOP 5问题。这里面需要做到以下几点:
    1. 利用倒排表来筛选 candidate
    2. 对于用户的输入 input_q,转换成句子向量
    3. 计算跟每个库里的问题之间的相似度
    4. 找出相似度最高的top5问题的答案
    """
    # EDIT START
    input_q = preprocess(input_q)
    top_k = 5
    top_idxs = []
    # 筛选
    idx = set()
    _ = [idx.update(inverted_idx[word]) for word in input_q.split() if word in inverted_idx]
    idx = list(idx)
    # 生成input_q的词向量
    input_q_vec = np.average([emb[word] for word in input_q.split() if word in emb], axis=0)
    # 计算相似度
    if np.linalg.norm(input_q_vec) == 0 or len(idx) == 0:
        return []
    candidate_X = X[idx]
    cosine_similarity = np.sum(np.multiply(candidate_X, input_q_vec), axis=1) / (np.linalg.norm(candidate_X, axis=1) * np.linalg.norm(input_q_vec))
    # 使用PriorityQueue
    que = PriorityQueue()
    _ = [que.put((-cosine, idx[index])) for index, cosine in enumerate(cosine_similarity)]
    for _ in range(top_k):
        top_idxs.append(que.get()[1])
    return alist[top_idxs]  # 返回相似度最高的问题对应的答案,作为TOP5答案    
    # EDIT END

# TODO: 编写几个测试用例,并输出结果
# EDIT START
# 原问题:How many records has Beyonce sold in her 19 year career? 答案:118 million
print (top5results_emb("How many records has Beyoncé sold?"))
# 原问题:Beyonce became the highest-paid black musical in which year? 答案:2014
print (top5results_emb("Which year did Beyonce become the highest-paid black musical?"))
# 微调阈值可能会对结果产生影响
# EDIT END

# 我们在验收作业时在后台会建立几个测试用例,来验证返回的准确性。
['118 million' '118 million' 'over 15 million' '300 million' '14 million']
['late 1990s' 'in the late 1990s' '2001' 'Paris' '2014']

参考

维特比最短路径问题 - 统计分词(unigram)

P.S.

# EDIT START
# EDIT START -> END: Edit by ziuno, for reference only.
# EDIT END
  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值