在-TensorFlow-上实现的-Word2Vec-教程

作者:chen_h
微信号 & QQ:862251340
微信公众号:coderpai


一个词嵌入的训练器

在这篇教程中,我们尝试使用深度学习模型来预测文本序列。但是,在我们利用深度学习处理文本之前,我们需要先学习一些 NLP 的基础信息。其中,一个比较重要的想法是将文字转化为数字向量,然后将数字向量输入到机器学习模型中。目前比较流行的处理方法是采用 word2vec 技术。

为什么我们需要 Word2Vec ?

如果我们想将单词提供给机器学习模型进行学习,除非我们采用基于树的方法,否则我们都需要将单词转换为一组数字向量。一个比较直接的做法是使用一个 0-1 稀疏向量来进行表示,该向量只有一个位置是 1,其余都是 0。这种稀疏表示方法在大多数分类任务中都取得了很好的效果。

因此,对于句子 the cat cat on the mat 我们可以得到如下的词向量表示:

上图中,我们把一个包含留个单词的句子转换成了 6*5 的向量,其中每一行代表每个单词对应的向量。然而,在实际的应用中,我们输入到机器学习或者深度学习模型中的词将是一个巨大的词库,比如 10000+ 维度的。你可以发现使用 0-1 向量来表示一个单词是一种非常低效的表示方法 —— 神经网络的输入层至少有 10000 个神经元节点。不仅如此,这种方法还剥夺了单词与单词(句子与句子)之间的关联性。

比如,我们希望看到 “United” 和 “States” 之间的距离很近,或者 “Soviet” 和 “Union”,或者 “food” 和 “eat” 之间的距离很近。0-1 方法丢失了所有这些信息,如果我们想要去构建一些自然语言模型,那么这将是一个巨大的漏洞。因此,我们需要一个更加有效的文本表示方法,可以保存一些文本之间的相关信息。这就是 Word2Vec 方法诞生的由来,该方法可以保存一些词之间的相关信息。

Word2Vec 技术

正如前面提到的,Word2Vec 方法需要解决两方面的事:第一,我们需要将一个高维度的 0-1 编码向量转换成一个低维度的向量。这个过程就称为词嵌入表示,比如我们将一个 1000 维度的 0-1 编码转换成一个 300 维度的向量。第二,我们需要保持词之间的上下文信息,从而在一定程度上实现词之间的相关性。在 Word2Vec 方法中,实现这两方面的目标有两种方法。第一种被称之为 skip-gram 方法,这个方法是输入一个词,然后去评估它周围的单词出现的概率。第二种被称之为 CBOW 方法,该方法和之前的方法刚刚相反,它是输入中心词附近的词,然后去预测中心词的概率。在这篇文章中,我们将重点介绍 skip-gram 方法。

那么,什么是 gram 呢?gram 是 n 个单词的组合,其中 n 是 gram 的窗口大小。比如句子 “the cat sat on the mat” ,那么这个句子的 3-gram 就可以表示为 “the cat sat”,“cat sat on”,“sat on the”,“on the mat”。skip 部分我们会在后续的内容中进行详细讲解。这些 grams 将会被输送到 Word2Vec 系统中。例如,我们假设输入的单词是 “cat” ,那么 Word2Vec 系统将会去预测上下文信息(“the”,“sat”)。Word2Vec 系统将学习单词的语义信息,为给定输入字的正确语境产生高概率预测。

那么,Word2Vec 系统怎么设计呢?它其实就是一个神经网络系统。

基于 softmax 的 Word2Vec 方法

考虑下面的图表,在这种情况下,我们假设句子“the cat sat on the mat” 是一个非常大的文本数据库中的一部分,长度大约是 10000 ,我们想要将这个高维度的向量转换成低维度的 300 维度的向量。

Word2Vec softmax 训练器

在上图中,如果我们输入的单词是“cat”,也就是说我们输入的是一个长度是 10000 的 0-1 向量。然后,我们把这个 10000 维度的向量和隐藏层之间的连接权重进行相乘,然后得到的向量再和下一层的权重进行相乘,以此类推。隐藏层中节点的激活我们采用简单的线性求和(即,不采用非线性激活函数,比如 sigmoid 或者 tanh)。这些节点的信息再被输送到最后层的 softmax 函数进行计算输出。在训练过程中,我们需要去修改神经网络的权重,使得单词“cat”周围的单词在 softmax 输出层中的概率更高。

训练这个网络,我需要构建一个 10000*300 的连接矩阵,来链接输入层和隐藏层。该矩阵的每一行表示对应一个单词表示,所以我们需要有效的将这 10000 维度的 0-1 向量压缩到 300 维度的向量。这个权重矩阵基本就可以看做是一个查询表。不仅如此,按照我们训练网络的方式,这个权重矩阵还包含了上下文信息。一旦我们完成了网络的训练,我们就可以放弃最后的 softmax 层,只需要使用前面的 10000*300 层的权重矩阵就行了。

那么代码中具体怎么实现呢?

TensorFlow 中的 softmax Word2Vec 方法

与任何机器学习问题一样,我们需要做两个步骤 —— 第一,我们需要将数据转换成模型可用的数据,第二是需要准备训练集,验证集和测试集。首先,我们将介绍如何处理数据,将数据转换成模型可用的数据,然后我们将设计模型的 TensorFlow 图。

准备文本数据

def maybe_download(filename, url, expected_bytes):
    """Download a file if not present, and make sure it's the right size."""
    if not os.path.exists(filename):
        filename, _ = urllib.request.urlretrieve(url + filename, filename)
    statinfo = os.stat(filename)
    if statinfo.st_size == expected_bytes:
        print('Found and verified', filename)
    else:
        print(statinfo.st_size)
        raise Exception(
            'Failed to verify ' + filename + '. Can you get to it with a browser?')
    return filename

这个函数先检查 filename 文件是否存在,如果不存在,那么这个函数就会利用 urllib.request 函数进行下载。如果文件已经存在了,也就是 os.path.exists(filename) 返回 true,那么函数就不会再次下载文件。接下来,该函数会检查文件的大小,并确保它与预期的文件大小 expected_bytes 相符。如果一切都顺利,它将返回可用于提取数据的文件名对象。要调用这个函数,我们可以采用如下代码:

url = 'http://mattmahoney.net/dc/'
filename = maybe_download('text8.zip', url, 31344016)

接下来,我们需要做的就是使用文件名来指向下载的文件,并且使用 zipfile 模块来进行数据提取。

# Read the data into a list of strings.
def read_data(filename):
    """Extract the first file enclosed in a zip file as a list of words."""
    with zipfile.ZipFile(filename) as f:
        data = tf.compat.as_str(f.read(f.namelist()[0])).split()
    return data

我们使用 zipfile.ZipFIle() 函数来提取 zip 文件。首先,namelist() 函数检索归档所有的文件 —— 在这里,我们只有一个文件,所有我们使用第 0 索引来访问。然后我们使用 read() 函数,该函数读取文件中的所有文本,并且通过 TensorFlow 的 as_str 函数将数据转换成字符串形式。最后,我们使用 split() 函数来创建一个包含文本文件中所有单词的列表,用空格字符分隔开来。我们可以得到如下输出:

vocabulary = read_data(filename)
print(vocabulary[:7])
['anarchism', 'originated', 'as', 'a', 'term', 'of', 'abuse']

你可以观察到,返回的词汇数据包含简单英文单词的列表,按原来文件中的句子顺序进行排列。现在,我们已经在列表中提取了所有的单词,我们接下来需要做进一步的处理,使我们能够创建一个 skip-gram 批处理数据。以下是具体的步骤:

  1. 抽取最多 10000 个常用的词,包含在我们的嵌入向量中。
  2. 收集所有独一无二的词,并用唯一的整数值对他们进行索引 —— 这就是为单词创建一个等效的 0-1 输入。我们利用字典来做到这一点。
  3. 循环遍历数据集中的每个单词(词汇变量),并将其分配给在上述步骤 2 中创建的唯一整数值,这将很容易查找和处理单词数据流。

执行上述功能的代码如下:

def build_dataset(words, n_words):
    """Process raw inputs into a dataset."""
    count = [['UNK', -1]]
    count.extend(collections.Counter(words).most_common(n_words - 1))
    dictionary = dict()
    for word, _ in count:
        dictionary[word] = len(dictionary)
    data = list()
    unk_count = 0
    for word in words:
        if word in dictionary:
            index = dictionary[word]
        else:
            index = 0  # dictionary['UNK']
            unk_count += 1
        data.append(index)
    count[0][1] = unk_count
    reversed_dictionary = dict(zip(dictionary.values(), dictionary.keys()))
    return data, count, dictionary, reversed_dictionary

第一步是数值一个 “计数器” 列表,它将存储在数据集中每个单词的次数。因为我们将词汇限制在 10000 字以内,所有词频不是在前 10000 个的词都会被赋予 “UNK” 标记,表示未知词。我们使用 Python 的 collections 模块中的 Counter() 类和相关的 most_common() 函数来设计初始化的计数列表。这些函数计算给定词库中的单词数,然后以列表形式返回最常见的 n 个单词。

这个函数的小一部分是创建一个字典(dictionary),它由一系列键值对组成。每个单词都被分配了一个独一无二的健值,健值按照单词数递增。举个例子,最常见的单词的健值为 1,第二常见的单词的健值为 2,第三常见的单词为 3,以此类推。“UNK” 被赋予健值 0 。此步骤为词库中每个单词创建一个唯一的健值 —— 也就是上述的第二步骤。

接下来,我们循环计算数据集中的每一个单词 —— 数据集是从函数 read_data() 中得到的,创建一个称为 data 的列表,它的长度和单词库相同。所以,对于我们第一句数据集 [‘anarchism’, ‘originated’, ‘as’, ‘a’, ‘term’, ‘of’, ‘abuse’],现在数据集的标记看起来是这样的 [5242, 3083, 12, 6, 195, 2, 3136] 。这部分解决了上面的第三步。

最后,该函数创建一个名为 reverse_dictionary 的字典,该字典允许我们根据其唯一的整数标记去查找单词。

现在我们需要建立一个包含我们的输入单词和上下文关联单词的数据集,这个数据集可以被用于训练我们的 Word2Vec 系统,具体代码如下:

data_index = 0
# generate batch data
def generate_batch(data, batch_size, num_skips, skip_window):
    global data_index
    assert batch_size % num_skips == 0
    assert num_skips <= 2 * skip_window
    batch = np.ndarray(shape=(batch_size), dtype=np.int32)
    context = np.ndarray(shape=(batch_size, 1), dtype=np.int32)
    span = 2 * skip_window + 1  # [ skip_window input_word skip_window ]
    buffer = collections.deque(maxlen=span)
    for _ in range(span):
        buffer.append(data[data_index])
        data_index = (data_index + 1) % len(data)
    for i in range(batch_size // num_skips):
        target = skip_window  # input word at the center of the buffer
        targets_to_avoid = [skip_window]
        for j in range(num_skips):
            while target in targets_to_avoid:
                target = random.randint(0, span - 1)
            targets_to_avoid.append(target)
            batch[i * num_skips + j] = buffer[skip_window]  # this is the input word
            context[i * num_skips + j, 0] = buffer[target]  # these are the context words
        buffer.append(data[data_index])
        data_index = (data_index + 1) % len(data)
    # Backtrack a little bit to avoid skipping words in the end of a batch
    data_index = (data_index + len(data) - span) % len(data)
    return batch, context

这个函数的作用是去产生训练过程中的最小批数据。这些批数据会被用作为输入数据,这些数据由随机相关的上下文单词组成。例如,我们有一个 5-gram 的数据 the cat sat on the ,中心词是 sat ,我们需要预测的上下文单词是 [the, cat, on, the]。在这个函数中,我们从中心词周围的上下文中随机抽取的单词数是由 num_skips 参数来实现的。输入单词的上下文窗口是由 skip_window 参数来实现的。比如在上面的例子中,我们设置的 skip_window = 2 ,所以我们中心词 sat 周围的上下文窗口是 the cat sat on the

在上面的函数中,首先将批处理的数据和标签输出定义在 batch_size 大小的变量中。然后,我们定义 span 参数,这个参数基本上就可以确定输入的中心词和它的上下文信息。比如句子 the cat sat on the 中,span 的大小是 5,即 5 = 2 skip_window + 1*。之后,我们可以创建如下的缓冲区:

buffer = collections.deque(maxlen=span)
for _ in range(span):
    buffer.append(data[data_index])
    data_index = (data_index + 1) % len(data)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值