问答机器人介绍

目标

  1. 知道问答机器人是什么
  2. 知道问答机器人实现的逻辑

1. 问答机器人

在前面的课程中,我们已经对问答机器人介绍过,这里的问答机器人是我们在分类之后,对特定问题进行回答的一种机器人。至于回答的问题的类型,取决于我们的语料。

当前我们需要实现的问答机器人是一个回答编程语言(比如python是什么python难么等)相关问题的机器人

2. 问答机器人的实现逻辑

主要实现逻辑:从现有的问答对中,选择出和问题最相似的问题,并且获取其相似度(一个数值),如果相似度大于阈值,则返回这个最相似的问题对应的答案

问答机器人的实现可以大致分为三步步骤:

  1. 对问题的处理
  2. 对答案进行的机器学习召回
  3. 对召回的结果进行排序

2.1 对问题的处理

对问题的处理过程中,我们可以考虑以下问题:

  1. 对问题进行基础的清洗,去除特殊符号等
  2. 问题主语的识别,判断问题中是否包含特定的主语,比如python等,提取出来之后,方便后续对问题进行过滤。
  • 可以看出,不仅需要对用户输入的问题进行处理,获取主语,还需要对现有问答对进行处理
  1. 获取问题的词向量,可以考虑使用词频,tdidf等值,方便召回的时候使用

2.2 问题的召回

召回:可以理解为是一个海选的操作,就是从现有的问答对中选择可能相似的前K个问题。

为什么要进行召回?

主要目的是为了后续进行排序的时候,减少需要计算的数据量,比如有10万个问答对,直接通过深度学习肯定是可以获取所有的相似度,但是速度慢。

所以考虑使用机器学习的方法进行一次海选

那么,如何实现召回呢?

前面我们介绍,召回就是选择前K个最相似的问题,所以召回的实现就是想办法通过机器学习的手段计算器相似度。

可以思考的方法:

  1. 使用词袋模型,获取词频矩阵,计算相似度
  2. 使用tfidf,获取tdidf的矩阵,计算相似度

上述的方法理论上都可行,知识当候选计算的词语数量太多的时候,需要挨个计算相似度,非常耗时。

所以可以考虑以下两点:

  1. 通过前面获取的主语,对问题进行过滤
  2. 使用聚类的方法,对数据先聚类,再计算某几个类别中的相似度,而不用去计算全部。

但是还有一个问题,供大家慢慢思考:

不管是词频,还是tdidf,获取的结果肯定是没有考虑文字顺序的,效果不一定是最好的,那么此时,应该如何让最后召回的效果更好呢?

2.3 问题的排序

排序过程,使用了召回的结果作为输入,同时输出的是最相似的那一个。

整个过程使用深度学习实现。深度学习虽然训练的速度慢,但是整体效果肯定比机器学习好(机器学习受限于特征工程,数据量等因素,没有办法深入的学会不同问题之间的内在相似度),所以通过自建的模型,获取最后的相似度。

使用深度学习的模型这样一个黑匣子,在训练数据足够多的时候,能够学习到用户的各种不同输入的问题,当我们把目标值(相似的问题)给定的情况下,让模型自己去找到这些训练数据目标值和特征值之间相似的表示方法。

那么此时,有以下两个问题:

  1. 使用什么数据,来训练模型,最后返回模型的相似度

训练的数据的来源:可以考虑根据现有的问答对去手动构造,但是构造的数据不一定能够覆盖后续用户提问的全部问题。所以可以考虑通过程序去采集网站上相似的问题,比如百度知道的搜索结果。

  1. 模型该如何构建

模型可以有两个输入,输出为一个数值,两个输入的处理方法肯定是一样的。这种网络结构我们经常把它称作孪生神经网络。

  1. 很明显,我们队输入的数据需要进行编码的操作,比如word embedding + LSTM/GRU/BIGRU等
    两个编码之后的结果,我们可以进行组合,然后通过一个多层的神经网络,输出一个数字,把这个数值定义为我们的相似度。
    当然我们的深层的神经网络在最开始的时候也并不是计算的相似度,但是我们的训练数据的目标值是相似度,在N多次的训练之后,确定了输入和输出的表示方法之后,那么最后的模型输出就是相似度了。

前面我们介绍了问答机器人的实现的大致思路,那么接下来,我们就来一步步的实现它

问答机器人的召回

目标

  1. 知道召回的目的
  2. 能够说出召回的流程
  3. 能够优化基础的召回逻辑

1. 召回的流程

流程如下:

  1. 准备数据,问答对的数据等
  2. 问题转化为向量
  3. 计算相似度

2. 对现有问答对的准备

这里说的问答对,是带有标准答案的问题,后续命中问答对中的问题后,会返回该问题对应的答案

为了后续使用方便,我们可以把现有问答对的处理成如下的格式,可以考虑存入数据库或者本地文件:

{
    "问题1":{
        "主体":["主体1","主体3","主体3"..],
        "问题1分词后的句子":["word1","word2","word3"...],
        "答案":"答案"
    },
    "问题2":{
        ...
    }
}

代码如下:

# lib/get_qa_dcit.py
def get_qa_dict():
    chuanzhi_q_path = "./问答对/Q.txt"
    chuanzhi_a_path = "./问答对/A.txt"
    QA_dict = {}
    for q,a in zip(open(chuanzhi_q_path).readlines(),open(chuanzhi_a_path).readlines()):
        QA_dict[q.strip()] = {}
        QA_dict[q.strip()]["ans"] = a.strip()
        QA_dict[q.strip()]["entity"] = sentence_entity(q.strip())[-1]

    #准备短问答的问题
    python_duan_path = "./data/Python短问答-11月汇总.xlsx"

    ret = pd.read_excel(python_duan_path)
    column_list = ret.columns
    assert '问题' in column_list and "答案" in column_list, "excel 中必须包含问题和答案"
    for q, a in zip(ret["问题"], ret["答案"]):
        q = re.sub("\s+", " ", q)
        QA_dict[q.strip()] = {}
        QA_dict[q.strip()]["ans"] = a
        cuted,entiry = sentence_entity(q.strip())[-1]
        QA_dict[q.strip()]["entity"] = entiry
        QA_dict[q.strip()]["q_cuted"] = cuted

    return QA_dict

QA_dict = get_qa_dict()

3. 把问题转化为向量

把问答对中的问题,和用户输出的问题,转化为向量,为后续计算相似度做准备。

这里,我们使用tfidf对问答对中的问题进行处理,转化为向量矩阵。

TODO,使用单字,使用n-garm,使用BM25,使用word2vec等,让其结果更加准确

from sklearn.feature_extraction.text import TfidfVectorizer
from lib import QA_dict

def build_q_vectors():
    """对问题建立索引"""
    lines_cuted= [q["q_cuted"] for q in QA_dict]
    tfidf_vectorizer = TfidfVectorizer()
    features_vec = tfidf_vectorizer.fit_transform(lines_cuted)
    #返回tfidf_vectorizer,后续还需要对用户输入的问题进行同样的处理
	return tfidf_vectorizer,features_vec,lines_cuted

4. 计算相似度

思路很简单。对用户输入的问题使用tfidf_vectorizer进行处理,然后和features_vec中的每一个结果进行计算,获取相似度。

但是由于耗时可能会很久,所以考虑使用其他方法来实现

4.1 pysparnn的介绍

官方地址:https://github.com/facebookresearch/pysparnn

pysparnn是一个对sparse数据进行相似邻近搜索的python库,这个库是用来实现 高维空间中寻找最相似的数据的。

4.2 pysparnn的使用方法

pysparnn的使用非常简单,仅仅需要以下步骤,就能够完成从高维空间中寻找相似数据的结果

  1. 准备源数据和待搜索数据
  2. 对源数据进行向量化,把向量结果和源数据构造搜索的索引
  3. 对待搜索的数据向量化,传入索引,获取结果
import pysparnn.cluster_index as ci

from sklearn.feature_extraction.text import TfidfVectorizer

#1. 原始数据
data = [
    'hello world',
    'oh hello there',
    'Play it',
    'Play it again Sam',
]  

#2. 原始数据向量化

tv = TfidfVectorizer()
tv.fit(data)

features_vec = tv.transform(data)

# 原始数据构造索引
cp = ci.MultiClusterIndex(features_vec, data)

# 待搜索的数据向量化
search_data = [
    'oh there',
    'Play it again Frank'
]

search_features_vec = tv.transform(search_data)

#3. 索引中传入带搜索数据,返回结果
cp.search(search_features_vec, k=1, k_clusters=2, return_distance=False)
>> [['oh hello there'], ['Play it again Sam']]

使用注意点:

  1. 构造索引是需要传入向量和原数据,最终的结果会返回源数据
  2. 传入待搜索的数据时,需要传入一下几个参数:
  1. search_features_vec:搜索的句子的向量
  2. k:最大的几个结果,k=1,返回最大的一个
  3. k_clusters:对数据分为多少类进行搜索
  4. return_distance:是否返回距离

4.3 使用pysparnn完成召回的过程

#构造索引
cp = ci.MultiClusterIndex(features_vec, lines_cuted)

#对用户输入的句子进行向量化
search_vec = tfidf_vec.transform(ret)
#搜索获取结果,返回最大的8个数据,之后根据`main_entiry`进行过滤结果
cp_search_list = cp.search(search_vec, k=8, k_clusters=10, return_distance=True)

exist_same_entiry = False
search_lsit = []
for _temp_call_line in cp_search_list[0]:
    cur_entity = QA_dict[_temp_call_line[1]]["main_entity"]
    if len(set(main_entity) & set(cur_entity))>0:  #命名体的集合存在交集的时候返回
        exist_same_entiry  = True
        search_lsit.append(_temp_call_line[1])

if exist_same_entiry: #存在相同的主体的时候
    return search_lsit
else:
    # print(cp_search_list)
    return [i[1] for i in cp_search_list[0]]

在这个过程中,需要注意,提前把cp,tfidf_vec等内容提前准备好,而不应该在每次接收到用户的问题之后重新生成一遍,否则效率会很低

4.4 pysparnn的原理介绍

参考地址:https://nlp.stanford.edu/IR-book/html/htmledition/cluster-pruning-1.html

前面我们使用的pysparnn使用的是一种cluster pruning(簇修剪)的技术,即,开始的时候对数据进行聚类,后续再有限个类别中进行数据的搜索,根据计算的余弦相似度返回结果。

数据预处理过程如下:

  1. 随机选择nlp 模型 召回率 精准率很高 但实际识别率很低 nlp14种回应术详解_pytorch个样本作为leader
  2. 选择非leader的数据(follower),使用余弦相似度计算找到最近的leader

当获取到一个问题q的时候,查询过程:

  1. 计算每个leader和q的相似度,找到最相似的leader
  2. 然后计算问题q和leader所在簇的相似度,找到最相似的k个,作为最终的返回结果

nlp 模型 召回率 精准率很高 但实际识别率很低 nlp14种回应术详解_pytorch_02

在上述的过程中,可以设置两个大于0的数字b1和b2

  • b1表示在数据预处理阶段,每个follower选择b1个最相似的leader,而不是选择单独一个lader,这样不同的簇是有数据交叉的;
  • b2表示在查询阶段,找到最相似的b2个leader,然后再计算不同的leader中下的topk的结果

前面的描述就是b1=b2=1的情况,通过增加b1和b2的值,我们能够有更大的机会找到更好的结果,但是这样会需要更加大量的计算。

在pysparnn中实例化索引的过程中

即:ci.MultiClusterIndex(features, records_data, num_indexes)中,num_indexes能够设置b1的值,默认为2。

在搜索的过程中,cp.search(search_vec, k=8, k_clusters=10, return_distance=True,num_indexes)num_Indexes可以设置b2的值,默认等于b1的值。

召回过程优化

目标

  1. 知道优化的方法和思路
  2. 知道BM25方法的原理和实现
  3. 能够使用word2vector完成优化过程

1. 优化思路

前面的学习,我们能够返回相似的召回结果,但是,如何让这些结果更加准确呢?

我们可以从下面的角度出发:

  1. tfidf使用的是词频和整个文档的词语,如果用户问题的某个词语没有出现过,那么此时,计算出来的相似度可能就不准确。该问题的解决思路:
  • 对用户输入的问题进行文本的对齐,比如,使用训练好的word2vector,往句子中填充非主语的其他词语的相似词语。例如python 好学 么 -->填充后是 :python 好学 么 简单 难 嘛 ,这里假设word2vector同学会了好学,简单,难他们之间是相似的
  • 使用word2vector对齐的好处除了应对未出现的词语,还能够提高主语的重要程度,让主语位置的tfidf的值更大,从而让相似度更加准确
  1. tfidf是一个词袋模型,没有考虑词和词之间的顺序
  • 使用n-garm和词一起作为特征,转化为特征向量
  1. 不去使用tfidf处理句子得到向量。
  • 使用BM25算法
  • 或者 使用fasttext、word2vector,把句子转化为向量

2. 通过BM25算法代替TFIDF

2.1 BM25算法原理

BM25(BM=best matching)是TDIDF的优化版本,首先我们来看看TFIDF是怎么计算的
nlp 模型 召回率 精准率很高 但实际识别率很低 nlp14种回应术详解_attention_03
其中tf称为词频,idf为逆文档频率

那么BM25是如何计算的呢?
nlp 模型 召回率 精准率很高 但实际识别率很低 nlp14种回应术详解_NLP_04
大家可以看到,BM25和tfidf的计算结果很相似,唯一的区别在于中多了一项,这一项是用来对tf的结果进行的一种变换。

nlp 模型 召回率 精准率很高 但实际识别率很低 nlp14种回应术详解_transformer_05中的b看成0,那么此时中间项的结果为nlp 模型 召回率 精准率很高 但实际识别率很低 nlp14种回应术详解_transformer_06,通过设置一个k,就能够保证其最大值为nlp 模型 召回率 精准率很高 但实际识别率很低 nlp14种回应术详解_attention_07,达到限制tf过大的目的。

即:
nlp 模型 召回率 精准率很高 但实际识别率很低 nlp14种回应术详解_attention_08
k不变的情况下,上式随着tf的增大而增大,上限为k+1,但是增加的程度会变小,如下图所示。

在一个句子中,某个词重要程度应该是随着词语的数量逐渐衰减的,所以中间项对词频进行了惩罚,随着次数的增加,影响程度的增加会越来越小。通过设置k值,能够保证其最大值为k+1,k往往取值1.2

其变化如下图(无论k为多少,中间项的变化程度会随着次数的增加,越来越小):

nlp 模型 召回率 精准率很高 但实际识别率很低 nlp14种回应术详解_智能问答_09

同时nlp 模型 召回率 精准率很高 但实际识别率很低 nlp14种回应术详解_transformer_05的作用是用来对文本的长度进行归一化。

例如在考虑整个句子的tdidf的时候,如果句子的长度太短,那么计算的总的tdidf的值是要比长句子的tdidf的值要低的。所以可以考虑对句子的长度进行归一化处理。

可以看到,当句子的长度越短,nlp 模型 召回率 精准率很高 但实际识别率很低 nlp14种回应术详解_pytorch_11的值是越小,作为分母的位置,会让整个第二项越大,从而达到提高短文本句子的BM25的值的效果。当b的值为0,可以禁用归一化,b往往取值0.75

其变化效果如下:

nlp 模型 召回率 精准率很高 但实际识别率很低 nlp14种回应术详解_NLP_12

2.2 BM25算法实现

通过前面的学习,我们知道其实BM25和Tfidf的区别不大,所以我们可以在之前sciket-learn的TfidfVectorizer基础上进行修改,获取我们的BM25的计算结果,主要也是修改其中的fit方法和transform方法

在sklearn的TfidfVectorizer中,首先接受参数,其次会调用TfidfTransformer来完成其他方法的调用

  1. 继承TfidfVectorizer完成 参数的接受
from sklearn.feature_extraction.text import TfidfVectorizer,TfidfTransformer,_document_frequency
from sklearn.base import BaseEstimator,TransformerMixin
from sklearn.preprocessing import normalize
from sklearn.utils.validation import check_is_fitted
import numpy as np
import scipy.sparse as sp

class Bm25Vectorizer(CountVectorizer):
    def __init__(self,k=1.2,b=0.75, norm="l2", use_idf=True, smooth_idf=True,sublinear_tf=False,*args,**kwargs):
        super(Bm25Vectorizer,self).__init__(*args,**kwargs)
        self._tfidf = Bm25Transformer(k=k,b=b,norm=norm, use_idf=use_idf,
                                       smooth_idf=smooth_idf,
                                       sublinear_tf=sublinear_tf)

    @property
    def k(self):
        return self._tfidf.k

    @k.setter
    def k(self, value):
        self._tfidf.k = value

    @property
    def b(self):
        return self._tfidf.b

    @b.setter
    def b(self, value):
        self._tfidf.b = value

    def fit(self, raw_documents, y=None):
        """Learn vocabulary and idf from training set.
        """
        X = super(Bm25Vectorizer, self).fit_transform(raw_documents)
        self._tfidf.fit(X)
        return self

    def fit_transform(self, raw_documents, y=None):
        """Learn vocabulary and idf, return term-document matrix.
        """
        X = super(Bm25Vectorizer, self).fit_transform(raw_documents)
        self._tfidf.fit(X)
        return self._tfidf.transform(X, copy=False)

    def transform(self, raw_documents, copy=True):
        """Transform documents to document-term matrix.
        """
        check_is_fitted(self, '_tfidf', 'The tfidf vector is not fitted')

        X = super(Bm25Vectorizer, self).transform(raw_documents)
        return self._tfidf.transform(X, copy=False)
  1. 完成自己的Bm25transformer,只需要再原来基础的代码上进心修改部分即可。sklearn中的转换器类的实现要求,不能直接继承已有的转换器类
class Bm25Transformer(BaseEstimator, TransformerMixin):

    def __init__(self,k=1.2,b=0.75, norm='l2', use_idf=True, smooth_idf=True,
                 sublinear_tf=False):
        self.k = k
        self.b = b
        ##################以下是TFIDFtransform代码##########################
        self.norm = norm
        self.use_idf = use_idf
        self.smooth_idf = smooth_idf
        self.sublinear_tf = sublinear_tf

    def fit(self, X, y=None):
        """Learn the idf vector (global term weights)

        Parameters
        ----------
        X : sparse matrix, [n_samples, n_features]
            a matrix of term/token counts
        """
        _X = X.toarray()
        self.avdl = _X.sum()/_X.shape[0] #句子的平均长度
        # print("原来的fit的数据:\n",X)

        #计算每个词语的tf的值
        self.tf = _X.sum(0)/_X.sum()  #[M] #M表示总词语的数量
        self.tf = self.tf.reshape([1,self.tf.shape[0]]) #[1,M]
        # print("tf\n",self.tf)
        ##################以下是TFIDFtransform代码##########################
        if not sp.issparse(X):
            X = sp.csc_matrix(X)
        if self.use_idf:
            n_samples, n_features = X.shape
            df = _document_frequency(X)

            # perform idf smoothing if required
            df += int(self.smooth_idf)
            n_samples += int(self.smooth_idf)

            # log+1 instead of log makes sure terms with zero idf don't get
            # suppressed entirely.
            idf = np.log(float(n_samples) / df) + 1.0
            self._idf_diag = sp.spdiags(idf, diags=0, m=n_features,
                                        n=n_features, format='csr')

        return self

    def transform(self, X, copy=True):
        """Transform a count matrix to a tf or tf-idf representation

        Parameters
        ----------
        X : sparse matrix, [n_samples, n_features]
            a matrix of term/token counts

        copy : boolean, default True
            Whether to copy X and operate on the copy or perform in-place
            operations.

        Returns
        -------
        vectors : sparse matrix, [n_samples, n_features]
        """
 		########### 计算中间项  ###############
        cur_tf = np.multiply(self.tf, X.toarray()) #[N,M] #N表示数据的条数,M表示总词语的数量
        norm_lenght = 1 - self.b + self.b*(X.toarray().sum(-1)/self.avdl) #[N] #N表示数据的条数
        norm_lenght = norm_lenght.reshape([norm_lenght.shape[0],1]) #[N,1]
        middle_part = (self.k+1)*cur_tf /(cur_tf +self.k*norm_lenght)
        ############# 结算结束  ################

        if hasattr(X, 'dtype') and np.issubdtype(X.dtype, np.floating):
            # preserve float family dtype
            X = sp.csr_matrix(X, copy=copy)
        else:
            # convert counts or binary occurrences to floats
            X = sp.csr_matrix(X, dtype=np.float64, copy=copy)

        n_samples, n_features = X.shape

        if self.sublinear_tf:
            np.log(X.data, X.data)
            X.data += 1
        if self.use_idf:
            check_is_fitted(self, '_idf_diag', 'idf vector is not fitted')

            expected_n_features = self._idf_diag.shape[0]
            if n_features != expected_n_features:
                raise ValueError("Input has n_features=%d while the model"
                                 " has been trained with n_features=%d" % (
                                     n_features, expected_n_features))
            # *= doesn't work
            X = X * self._idf_diag
		
        ############# 中间项和结果相乘  ############
        X = X.toarray()*middle_part
        if not sp.issparse(X):
            X = sp.csr_matrix(X, dtype=np.float64)
        ############# #########
        
        if self.norm:
            X = normalize(X, norm=self.norm, copy=False)

        return X

    @property
    def idf_(self):
        ##################以下是TFIDFtransform代码##########################
        # if _idf_diag is not set, this will raise an attribute error,
        # which means hasattr(self, "idf_") is False
        return np.ravel(self._idf_diag.sum(axis=0))

完整代码参考:https://github.com/SpringMagnolia/Bm25Vectorzier/blob/master/BM25Vectorizer.py

  1. 测试简单使用,观察和tdidf的区别:
from BM25Vectorizer import Bm25Vectorizer
from sklearn.feature_extraction.text import TfidfVectorizer


if __name__ == '__main__':
    # format_weibo(word=False)
    # format_xiaohuangji_corpus(word=True)
    bm_vec = Bm25Vectorizer()
    tf_vec = TfidfVectorizer()
    # 1. 原始数据
    data = [
        'hello world',
        'oh hello there',
        'Play it',
        'Play it again Sam,24343,123',
    ]

    # 2. 原始数据向量化
    bm_vec.fit(data)
    tf_vec.fit(data)
    features_vec_bm = bm_vec.transform(data)
    features_vec_tf = tf_vec.transform(data)
    print("Bm25 result:",features_vec_bm.toarray())
    print("*"*100)
    print("Tfidf result:",features_vec_tf.toarray())

输出如下:

Bm25 result: [[0.         0.         0.         0.47878333 0.         0.
  0.         0.         0.         0.8779331 ]
 [0.         0.         0.         0.35073401 0.         0.66218791
  0.         0.         0.66218791 0.        ]
 [0.         0.         0.         0.         0.70710678 0.
  0.70710678 0.         0.         0.        ]
 [0.47038081 0.47038081 0.47038081 0.         0.23975776 0.
  0.23975776 0.47038081 0.         0.        ]]
**********************************************************************************
Tfidf result: [[0.         0.         0.         0.6191303  0.         0.
  0.         0.         0.         0.78528828]
 [0.         0.         0.         0.48693426 0.         0.61761437
  0.         0.         0.61761437 0.        ]
 [0.         0.         0.         0.         0.70710678 0.
  0.70710678 0.         0.         0.        ]
 [0.43671931 0.43671931 0.43671931 0.         0.34431452 0.
  0.34431452 0.43671931 0.         0.        ]]

2.3 修改之前的召回代码

修改之前召回的代码只需要把调用tfidfvectorizer改成调用Bm25vectorizer

3. 使用Fasttext实现获取句子向量

3.1 基础方法介绍

这里我们可以使用fasttext,word2vector等方式实现获取词向量,然后对一个句子中的所有词语的词向量进行平均,获取整个句子的向量表示,即sentence Vector,该实现方法在fasttext和Word2vector中均有实现,而且通过参数的控制,实现N-garm的效果

假设我们有文本a.txt如下:

我 很 喜欢 她 
今天 天气 不错
我 爱 深度学习

那么我们可以实现获取句子向量的方法如下

from fastText import FastText
#训练模型,设置n-garm=2
model = FastText.train_unsupervised(input="./a.txt",minCount=1,wordNgrams=2)
#获取句子向量,是对词向量的平均
model.get_sentence_vector("我 是 谁")

3.2 训练模型和封装代码

这里我们使用之前采集的相似文本数据作为训练样本

步骤如下:

  1. 进行分词之后写入文件中
  2. 进行模型的训练
  3. 使用模型获取句子向量,并且封装代码
  4. 将之前的BM25的代码替换为该代码
3.2.1 分词写入文件

这里我们使用单个字作为特征,只需要注意,英文使用单个词作为特征

"""
使用单个字作为特征,进行fasttext训练,最后封装代码获取召回结果
"""
import string


def word_split(line):
    #对中文按照字进行处理,对英文不分为字母
    #即 I爱python --> i 爱 python
    letters = string.ascii_lowercase+"+"+"/"  #c++,ui/ue
    result = []
    temp = ""
    for word in line:
        if word.lower() in letters:
            temp+=word.lower()
        else:
            if temp !="":
                result.append(temp)
                temp = ""
            result.append(word)
    if temp!="":
        result.append(temp)
    return result

def process_data():
    path1 = r"corpus\final_data\merged_q.txt"
    path2 = r"corpus\final_data\merged_sim_q.txt"
    save_path =  r"corpus\recall_fasttext_data\data.txt"

    filter = set()
    with open(path1) as f,open(save_path,"a") as save_f:
        for line in f:
            line = line.strip()
            if line not in filter:
                filter.add(line)
                _temp = " ".join(word_split(line))
                save_f.write(_temp+"\n")

    with open(path2) as f,open(save_path,"a") as save_f:
        for line in f:
            line = line.strip()
            if line not in filter:
                filter.add(line)
                _temp = " ".join(word_split(line))
                save_f.write(_temp+"\n")
3.2.2 训练模型
  1. 训练fasttext的model,用来生成词向量
def train_model(fasttext_model_path):
 logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
 save_path =  r"corpus\recall_fasttext_data\data.txt"

 model = FastText.train_unsupervised(save_path,epoch=20,minCount=3,wordNgrams=2)
 model.save_model(fasttext_model_path)
  1. 对现有的QA问答对,生成向量,传入pysparnn中构建索引
def get_base_text_vectors(cp_dump_path,model):
    #保存到本地pkl文件,防止每次都生成一次
    if os.path.exists(cp_dump_path):
        cp = pickle.load(open(cp_dump_path,"rb"))
    else:
        print(QA_dict)
        q_lines = [q for q in QA_dict]
        q_cuted_list = [" ".join(word_split(i)) for i in q_lines]
        lines_vectors = []
        for q_cuted in q_cuted_list:
            lines_vectors.append(model.get_sentence_vector(q_cuted))
        cp = ci.MultiClusterIndex(lines_vectors,q_lines)
        pickle.dump(cp,open(cp_dump_path,"wb"))
    return cp
  1. 传入用户的问题,进行分词和句子向量的获取,获取搜索的结果
def get_search_vectors(cp,model,search_line):
    line_cuted = " ".join(word_split(search_line))
    line_vec = model.get_sentence_vector(line_cuted)
    #这里的line_vec中可以有多个句子的向量表示,能够返回每个句子的搜索结果
    cp_search_list = cp.search(line_vec,k=10,k_clusters=10,return_distance=True)
    #TODO 对搜索的结果进行关键字的过滤
    return cp_search_list
  1. 测试模型的效果
from fastext_vectors import get_search_vectors,train_model,get_base_text_vectors
import fastText

if __name__ == '__main__':
    fasttext_model_path = "corpus/build_questions/fasttext_recall.model"
    cp_dump_path = "corpus/build_questions/cp_recall.pkl"
    
    # train_model(fasttext_model_path)
    
    model = fastText.load_model(fasttext_model_path)

    cp = get_base_text_vectors(cp_dump_path,model)

    ret = get_search_vectors(cp,model,"女孩学python容易么?")
    print(ret)

输出如下:

[[('0.0890376', '学习Python需要什么基础,学起来更容易?'), 
  ('0.090688944', '学习PHP的女生多吗?女生可以学吗?'), 
  ('0.092773676', 'Python适合什么人学习?'), 
  ('0.09416294', 'Python语言适合什么样的人学?'), 
  ('0.102790296', 'python语言容易学习吗?'), 
  ('0.1050359', '学习测试的女生多吗?女生可以学吗?'), 
  ('0.10546541', 'Python好学吗?'), 
  ('0.11058545', '学习Python怎样?'), 
  ('0.11080605', '怎样学好Python?'), 
  ('0.11124289', '学生怎么上课的?')]]

3.2.3 基础封装

#lib/SentenceVectorizer
"""
使用fasttext 实现sentence to vector
"""
import fastText
from fastText import FastText
import config
from lib import cut
import logging
import os

class SentenceVectorizer:
    def __init__(self):
        if os.path.exists(config.recall_fasttext_model_path):
            self.model = fastText.load_model(config.recall_fasttext_model_path)
        else:
            # self.process_data()
            self.model = self.build_model()

        self.fited = False


    def fit_transform(self,sentences):
        """处理全部问题数据"""
        lines_vectors = self.fit(sentences)
        return lines_vectors

    def fit(self,lines):
        lines_vectors = []
        for q_cuted in lines:
            lines_vectors.append(self.model.get_sentence_vector(q_cuted))
        self.fited = True
        return lines_vectors

    def transform(self,sentence):
        """处理用户输入的数据"""
        assert self.fited = True
        line_vec = self.model.get_sentence_vector(" ".join(sentence))
        return line_vec


    def build_model(self):
        logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
        model = FastText.train_unsupervised(config.recall_fasttext_data_path, epoch=20, minCount=3, wordNgrams=2)
        model.save_model(config.recall_fasttext_model_path)
        return model

    def process_data(self):
        path1 = r"corpus\final_data\merged_q.txt"
    	path2 = r"corpus\final_data\merged_sim_q.txt"
    	save_path =  r"corpus\recall_fasttext_data\data.txt"

        filter = set()
        with open(path1) as f, open(save_path, "a") as save_f:
            for line in f:
                line = line.strip()
                if line not in filter:
                    filter.add(line)
                    _temp = " ".join(cut(line,by_word=True))
                    save_f.write(_temp + "\n")

        with open(path2) as f, open(save_path, "a") as save_f:
            for line in f:
                line = line.strip()
                if line not in filter:
                    filter.add(line)
                    _temp = " ".join(cut(line,by_word=True))
                    save_f.write(_temp + "\n")