65.9K
CodeProject 正在变化。 阅读更多。
Home

Lina 聊天机器人 - 使用 TF-IDF 文档检索生成响应

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2017年9月10日

CPOL

11分钟阅读

viewsIcon

21313

downloadIcon

483

使用 TF-IDF 构建基于检索的聊天机器人。

系列介绍

这将是关于自然语言和机器学习的多个教程系列,我们将手动创建一个聊天机器人表格,以便

  • 与用户聊天
  • 回答他们的问题
  • 提取关于用户的数据
  • 执行一些操作(推荐电影,告知时间...)

本系列的目标不仅仅是创建一个聊天机器人,而是展示一个可以使用 NLP 和 ML 实现的实际项目,因为我的主要目标是以有趣的方式(即创建一个聊天机器人)来展示 ML 和 NLP。

在本系列中,我们将介绍一些简单和高级的技术,以达到我们的最终目标。它们是

  • 使用 TF-IDF 进行文档检索
  • 使用 Word2Vect 进行文档检索
  • CKY 解析
  • 网络爬取

我希望通过这个系列,您会通过这个有趣的项目爱上 NLP 和 ML。

我们想称我们的聊天机器人为 Lina :D

教程介绍

要构建聊天机器人,有两种主要方法:生成方法和检索方法。对于本系列,我们将使用检索方法,因为它既简单又实用,可以输出非常好的结果

在本教程中,我们将讨论如何使用文档检索让 Lina 与用户聊天,在接下来的教程中,我们将讨论如何使其更具交互性。

使用检索方法构建时,有一些通用步骤

  1. 定义一些句子及其回复(数据收集步骤),我从一个名为 rDanny 的在线仓库中附上了一个简单的句子回复。
  2. 使用 TF-IDF(词频-逆文档频率)教您的聊天机器人此数据集。
  3. 使用余弦相似度比较输入句子和学习到的内容。

附件的 zip 包含代码和数据集。

议程

在本教程中,我们将

  1. (可选)简要讨论 TF-IDF(词频-逆文档频率)的概念
  2. (可选)然后讨论什么是 余弦相似度 以及我们为什么使用它
  3. 您可以跳过技术背景,直接创建您的聊天机器人)然后我们来到有趣的部分,即实际构建 Lina(我们的聊天机器人 :D)

大多数技术细节来自 Dan Jurafsky 和 Chris 的 YouTube 播放列表。

使用的数据集来自 rDanny

那么,让我们开始吧!

1. TF-IDF

tf-idf 代表词频-逆文档频率,它是一种简单的方法,旨在为与用户查询相似的文档打分。

它建立在两个主要概念之上

  1. 这个词(在查询中)在每个文档中出现了多少次 ==> 词频
  2. 这个词的独特性如何 ==> 逆文档频率

1.a 词频

它是一个给定文档中特定词出现的简单计数,我们计算所有给定文档中的所有词 ==> 为文档打分。

示例

这里,在这个例子中,我们有多个文档,这里是莎士比亚的戏剧,我们通过这些文档测试特定词的出现次数,因为这里只关注词频时,我们只关心词计数

现在我们需要只根据词频(词计数)来衡量分数,在接下来的部分中,我们将利用我们计算分数的方式,但是

原始词频不是我们想要的,因为

  • 一个包含该词出现 10 次的文档比包含该词出现 1 次的文档更相关。
  • 但并非相关 10 倍。

因为相关性不会与词频成比例增加。所以我们将使用对数频率而不是简单的词计数

请记住,`tf` 只是词计数,所以只要 `tf > 0`,我们就会将分数计算为 `1+Log`,否则如果词计数为零 (`tf=0`),分数将为零。所以现在来计算分数。

词频不足以说明问题

但是我们必须考虑到某个词与其他词在所有文档中的稀有程度。因此,为了完成 TF-IDF,我们必须考虑文档频率,或者更确切地说,它的倒数,以考虑词的稀有性

1.b 逆文档频率

在这里,我们倾向于衡量词的稀有性,这是通过计算一个词在多少个文档中出现来完成的。一个词在一个文档中出现一次,即使它在该文档中出现多次,我们也只计数一次,因为我们这里的主要目标是计算文档而不是词。

示例

假设有一百万个文档,您查找一个词列表,并查看这些词在多少个文档中出现。

df 在这里代表文档数量。所以这里像“calpurnia”这样的词只在一个文档中出现(非常罕见),所以它会得到高分,所以小的 df(文档计数)得到大的分数,所以我们得到文档频率的倒数

因此,为以上所有术语(词)计算此值后,结果将是

注意

正如您在这里看到的,IDF(逆文档频率)独立于查询,因为它只依赖于文档,所以我们不需要为每个新查询计算 idf,因为每个术语都有一个 idf 值。这可以用于优化在查询之前计算 idf

因此,如果我们只想根据 IDF(逆文档频率)来衡量分数

1.c 将 TF 和 IDF 结合到简单的分数计算中

此分数计算需要

  • 增加出现次数 (`TermFreq`)
  • 增加词的稀有性(逆文档频率)

我们首先计算特定文档中特定术语的权重。

将其应用于莎士比亚戏剧的先前示例

现在每个文档都由一个实值 TF-IDF 权重向量表示。

现在计算特定查询与特定文档的分数。

t==> 术语(词),q==> 查询,d==> 文档

因此,您计算查询 (q) 中所有术语 (t) 的分数(如果给定文档中未找到此术语,则 tf = 0)。然后将查询中所有术语的分数相加,以获得此文档的分数。

注意

现在知道如何计算特定文档的分数后,为了进行文档检索,我们需要对所有文档执行此方法,但我们倾向于使用向量空间的概念,而不仅仅是简单的分数公式来获得相似性,所以我们将查询和文档都表示在一个向量空间中,以便使用相似性计算

1.d 为什么选择向量空间模型??

在前面的例子中,我们可以说每个文档现在都变成了一个实值向量。

以《尤利乌斯·凯撒》为例,它的向量将是 ==>[ 3.18 6.1 2.54 1.54 0 0 0 ]

我们可以将词(术语)视为我们空间的维度,所以现在文档就像空间中的一个点(或一个向量),我们也可以以同样的方式看待查询,将其视为空间中的一个点(向量)。

但是这个空间的维度可能会非常高(术语数量可能达到数千万),因为请记住,我们的维度取决于您拥有的词汇量,所以我们需要一种比运行前面方法更有效的方法

对于我们的 10000 个文档(假设我们的词汇量是 1000 万,并且我们有 10000 个文档),运行 1000 万次,这就需要使用向量空间概念的方法,其中之一可以简单地被视为计算大维度空间中两个向量(特定文档和查询)之间最小距离的方法,这称为欧几里得距离

在这里,我们看到,这是一个只有两个词(八卦和嫉妒)的微小空间,我们有 3 个文档和 1 个查询,这 3 个文档

  • d1 == > 更多与八卦有关
  • d3 == > 更多与嫉妒有关
  • d2 == > 包含两个术语

这里显示 q(我们的查询)想要一个包含八卦和嫉妒的文档 (d2),但欧几里得距离不会返回 d2,因为从 q(向量的尖端)到 d2(d2 的尖端)的距离大于 d1 和 d3 的距离。

所以我们使用测量角度而不是测量距离的概念。

因此,这里就用到了余弦相似度

2. 余弦相似度

2.a 为什么是余弦相似度?

有许多方法可以使用向量空间的概念,但我们特别使用余弦相似度,因为在使用向量空间时,我们会面临一个问题,即不同长度的文档会导致错误的分数(如前所述),为了解决这个问题,我们必须考虑使用长度归一化的概念,这就是我们使用余弦相似度的原因

2.b 长度归一化

通过将其每个分量除以其长度,可以对向量进行长度归一化——为此,我们使用 L2 范数

现在,长文档和短文档具有可比较的权重。

2.c 为什么要使用余弦函数

我们使用余弦相似度是因为余弦是区间 [0o, 180o] 上的单调递减函数,范围从 1 → -1

以下两个概念是等价的。

  • 按查询与文档之间角度的降序对文档进行排名
  • 根据余弦函数的本质,按余弦(查询,文档)的升序对文档进行排名

2.d 余弦相似度计算

现在让我们真正使用余弦相似度来计算相似度

请记住,我们为查询和特定文档计算 TF-IDF,然后使用余弦方法获得相似度,因此我们主要需要两个变量 q 和 d

qi 是术语 i 在查询中的 TF-IDF 权重
di 是术语 i 在文档中的 TF-IDF 权重

但是

我们正在处理归一化长度,因此公式可以简化为只有点积(或标量积)

示例

您可以将其形象化为

示例

3. 让我们来构建我们的聊天机器人!

以上所有解释都只是为了了解 TF-IDF 背后的概念,因为已经有很多免费工具实现了 TF-IDF,关键不是重新创建 TF-IDF,而是如何将其有效地利用。

我们的聊天机器人需要一个包含句子及其回复的数据集。

我们的一般步骤是

  1. 其中每个句子都将被视为一个独立的文档
  2. 然后当用户输入他的聊天内容时,我们将其视为我们的查询
  3. 然后我们使用 TF-IDF 和余弦相似度来比较两者,并从数据集中获取与查询最相似的句子并输出回复

那么,让我们开始编码吧!

首先,我们需要导入

  1. 我们将主要使用 sklearn 和 numpy 库 (TF-IDF 库)
  2. 我们还将使用其他一些库,它们的功能稍后会出现 (辅助库)
# _____TF-IDF libraries_____
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.feature_extraction.text import TfidfVectorizer
import numpy as np

# _____helper Libraries_____
import pickle  # would be used for saving temp files
import csv     # used for accessing the dataset
import timeit  # to measure time of training
import random  # used to get a random number

现在让我们创建一个函数,它接受一个 `string` 作为输入并输出响应,我们所有的工作都将在该函数内部进行。

让我们定义 csv 文件的路径,我们将其放在名为 DataSet 的文件夹中。

def talk_to_lina_primary(test_set_sentence):
   
    csv_file_path = "DataSet/randychat.csv"

为了使用 `sklearn`,我们必须将字符串聊天加载到字典中,所以,我们将创建一个数组来加载 csv 文件,因此我们将创建一个名为 `sentences` 的数组。

def talk_to_lina_primary(test_set_sentence):
   
    csv_file_path = "DataSet/randychat.csv"

    i = 0
    sentences = []

    # enter your test sentence
    test_set = (test_set_sentence, "")

我们的代码将分为两个部分,一个用于训练,另一个用于运行代码。我们将使用临时文件保存我们的对象,我们将这些临时文件保存在 DataSet 文件夹中。因此,我们将为此使用 try except 子句,并将运行阶段放在 `try` 部分,以便在没有临时文件时,我们将进入 `except` 部分。

def talk_to_lina_primary(test_set_sentence):
   
    csv_file_path = "DataSet/randychat.csv"

    # we would use 2 temp files
    tfidf_vectorizer_pikle_path = "DataSet/tfidf_vectorizer.pickle"
    tfidf_matrix_train_pikle_path ="DataSet/tfidf_matrix_train.pickle"

    i = 0
    sentences = []

    test_set = (test_set_sentence, "")

    # for indexes
    sentences.append(" No you.")
    sentences.append(" No you.")

    try:
        ##--------------to use------------------#
        # ----------------------------------------#
    except:
        # ---------------to train------------------#
        # -----------------------------------------#

对于训练,我们会注意到,

  1. 识别空间的维度独立于查询,并且
  2. 学习数据集的 TF-IDF 也独立于查询

所以我们可以学习它们一次,然后将它们保存到两个临时文件中

def talk_to_lina_primary(test_set_sentence):
   
    csv_file_path = "DataSet/randychat.csv"
    tfidf_vectorizer_pikle_path = "DataSet/tfidf_vectorizer.pickle"
    tfidf_matrix_train_pikle_path ="DataSet/tfidf_matrix_train.pickle"

    i = 0
    sentences = []

    # enter your test sentence
    test_set = (test_set_sentence, "")

    # for indexes
    sentences.append(" No you.")
    sentences.append(" No you.")

    try:
        ##--------------to use------------------#
        # ----------------------------------------#
    except:
        # ---------------to train------------------#
        # variable to see how much time it took to train 
        start = timeit.default_timer()

        # to load rows of csv into sentences array
        with open(csv_file_path, "r") as sentences_file:
            reader = csv.reader(sentences_file, delimiter=',')
            for row in reader:
                sentences.append(row[0])
                i += 1
    
        # now we would start on with training
        # begin with identing a TfidfVectorizer obj
        tfidf_vectorizer = TfidfVectorizer()

        # this line does both
        # 1- identify dimension of the space
        # 2- learn tf-idf of the dataset
        tfidf_matrix_train = tfidf_vectorizer.fit_transform(sentences) 


        # now the training is finished
      
        # now simply record time that it finished training
        stop = timeit.default_timer()
        print ("training time took was : ")
        print stop - start

        # then we would save these 2 objs (dimension space and tf-idf to temp files)
        # we use pickle lib

        # first save dimension space
        f = open(tfidf_vectorizer_pikle_path, 'wb')
        pickle.dump(tfidf_vectorizer, f)
        f.close()

        # then save tf-idf of dataset
        f = open(tfidf_matrix_train_pikle_path, 'wb')
        pickle.dump(tfidf_matrix_train, f)
        f.close()
        # -----------------------------------------#

然后我们将处理使用(学习后)的部分

def talk_to_lina_primary(test_set_sentence):
   
    csv_file_path = "DataSet/randychat.csv"
    tfidf_vectorizer_pikle_path = "DataSet/tfidf_vectorizer.pickle"
    tfidf_matrix_train_pikle_path ="DataSet/tfidf_matrix_train.pickle"

    i = 0
    sentences = []
    test_set = (test_set_sentence, "")

    # for indexes
    sentences.append(" No you.")
    sentences.append(" No you.")

    try:
        ##--------------to use------------------#
        # first load dimension space
        f = open(tfidf_vectorizer_pikle_path, 'rb')
        tfidf_vectorizer = pickle.load(f)
        f.close()

        # then load tf-idf of dataset
        f = open(tfidf_matrix_train_pikle_path, 'rb')
        tfidf_matrix_train = pickle.load(f)
        f.close()
        # ----------------------------------------#
    except:
        # ---------------to train------------------#
        start = timeit.default_timer()

        # enter jabberwakky sentence
        with open(csv_file_path, "r") as sentences_file:
            reader = csv.reader(sentences_file, delimiter=',')
            for row in reader:
                sentences.append(row[0])
                i += 1

        tfidf_vectorizer = TfidfVectorizer()
        tfidf_matrix_train = tfidf_vectorizer.fit_transform(sentences) 
        stop = timeit.default_timer()
        print ("training time took was : ")
        print stop - start

        f = open(tfidf_vectorizer_pikle_path, 'wb')
        pickle.dump(tfidf_vectorizer, f)
        f.close()

        f = open(tfidf_matrix_train_pikle_path, 'wb')
        pickle.dump(tfidf_matrix_train, f)
        f.close()
        # -----------------------------------------#

使用和训练的两个部分都输出两个对象,然后我们使用这两个对象来实际实现相似度计算。

我们需要

  1. 使用学习到的维度空间对查询运行 TF-IDF
  2. 在查询的 TF-IDF 和数据集的 TF-IDF 之间运行余弦相似度
def talk_to_lina_primary(test_set_sentence):
   
    csv_file_path = "DataSet/randychat.csv"
    tfidf_vectorizer_pikle_path = "DataSet/tfidf_vectorizer.pickle"
    tfidf_matrix_train_pikle_path ="DataSet/tfidf_matrix_train.pickle"

    i = 0
    sentences = []

    # enter your test sentence
    test_set = (test_set_sentence, "")

    # 3ashan yzabt el indexes
    sentences.append(" No you.")
    sentences.append(" No you.")

    try:
        ##--------------to use------------------#
        f = open(tfidf_vectorizer_pikle_path, 'rb')
        tfidf_vectorizer = pickle.load(f)
        f.close()

        f = open(tfidf_matrix_train_pikle_path, 'rb')
        tfidf_matrix_train = pickle.load(f)
        f.close()
        # ----------------------------------------#
    except:
        # ---------------to train------------------#
        start = timeit.default_timer()

        # enter jabberwakky sentence
        with open(csv_file_path, "r") as sentences_file:
            reader = csv.reader(sentences_file, delimiter=',')
            # reader.next()
            # reader.next()
            for row in reader:
                # if i==stop_at_sentence:
                #    break
                sentences.append(row[0])
                i += 1

        tfidf_vectorizer = TfidfVectorizer()
        tfidf_matrix_train = tfidf_vectorizer.fit_transform(sentences)  # finds the tfidf score with normalization
        # tfidf_matrix_test =tfidf_vectorizer.transform(test_set)
        stop = timeit.default_timer()
        print ("training time took was : ")
        print stop - start

        f = open(tfidf_vectorizer_pikle_path, 'wb')
        pickle.dump(tfidf_vectorizer, f)
        f.close()

        f = open(tfidf_matrix_train_pikle_path, 'wb')
        pickle.dump(tfidf_matrix_train, f)
        f.close()
        # -----------------------------------------#

    # use the learnt dimension space
    # to run TF-IDF on the query
    tfidf_matrix_test = tfidf_vectorizer.transform(test_set)

    # then run cosine similarity between the 2 tf-idfs
    cosine = cosine_similarity(tfidf_matrix_test, tfidf_matrix_train)
    cosine = np.delete(cosine, 0)
    
    # then get the max score
    max = cosine.max()
    response_index = 0

    # if score is more than 0.7
    if (max > 0.7): 
        # we can afford to get multiple high score documents to choose from
        new_max = max - 0.01
        
        # load them to a list
        list = np.where(cosine > new_max)
        
        # choose a random one to return to the user 
        # this happens to make Lina answers differently to same sentence
        response_index = random.choice(list[0])

    else:
        # else we would simply return the highest score
        response_index = np.where(cosine == max)[0][0] + 2 
       

    j = 0

    # loop to return the next cell on the row , ( the response cell )
    with open(csv_file_path, "r") as sentences_file:
        reader = csv.reader(sentences_file, delimiter=',')
        for row in reader:
            j += 1  # we begin with 1 not 0 &    j is initialized by 0
            if j == response_index:
                return row[1], response_index,
                break

我们只是在一个简单的界面中调用这个函数。我们创建的函数既返回响应行又返回响应本身,我们只会向用户显示响应字符串

while 1:
    sentence = raw_input("talk to Lina : ")

    response_primary, line_id_primary = talk_to_lina_primary(sentence)
    print response_primary
    print

希望这很有趣,等待下一个教程。它会更令人兴奋,我们将使 Lina 能够回答用户的问题,我们将使用

  1. CKY 解析
  2. 简单正则表达式
  3. 使用 BeautifulSoup 进行网页抓取
  • DuckDuckGo
  • 雅虎问答

历史

  • 2017 年 9 月 10 日:初始版本
  • 2021 年 3 月 11 日:更新
© . All rights reserved.