词嵌入向量WordEmbedding的原理和生成方法

姓名:崔少杰       学号:16040510021

转载自:http://www.jianshu.com/p/0bb00eed9c63=有修改

【嵌牛导读】:词嵌入向量(WordEmbedding)是NLP里面一个重要的概念,我们可以利用WordEmbedding将一个单词转换成固定长度的向量表示,从而便于进行数学处理。本文将介绍WordEmbedding的使用方式,并讲解如何通过神经网络生成WordEmbedding。

【嵌牛鼻子】:WordEmbedding

【嵌牛提问】:词嵌入向量WordEmbedding在解决问题时有什么巨大的作用?

【嵌牛正文】:WordEmbedding的使用

使用数学模型处理文本语料的第一步就是把文本转换成数学表示,有两种方法,第一种方法可以通过one-hot矩阵表示一个单词,one-hot矩阵是指每一行有且只有一个元素为1,其他元素都是0的矩阵。针对字典中的每个单词,我们分配一个编号,对某句话进行编码时,将里面的每个单词转换成字典里面这个单词编号对应的位置为1的one-hot矩阵就可以了。比如我们要表达“the cat sat on the mat”,可以使用如下的矩阵表示。

one-hot矩阵表示法

one-hot表示方式很直观,但是有两个缺点,第一,矩阵的每一维长度都是字典的长度,比如字典包含10000个单词,那么每个单词对应的one-hot向量就是1X10000的向量,而这个向量只有一个位置为1,其余都是0,浪费空间,不利于计算。第二,one-hot矩阵相当于简单的给每个单词编了个号,但是单词和单词之间的关系则完全体现不出来。比如“cat”和“mouse”的关联性要高于“cat”和“cellphone”,这种关系在one-hot表示法中就没有体现出来。

WordEmbedding解决了这两个问题。WordEmbedding矩阵给每个单词分配一个固定长度的向量表示,这个长度可以自行设定,比如300,实际上会远远小于字典长度(比如10000)。而且两个单词向量之间的夹角值可以作为他们之间关系的一个衡量。如下表示:

WordEmbedding表示法

通过简单的余弦函数,我们就可以计算两个单词之间的相关性,简单高效:

两个向量相关性计算

因为WordEmbedding节省空间和便于计算的特点,使得它广泛应用于NLP领域。接下来我们讲解如何通过神经网络生成WordEmbedding。

WordEmbedding的生成

WordEmbedding的生成我们使用tensorflow,通过构造一个包含了一个隐藏层的神经网络实现。

下面是下载数据和加载数据的代码,一看就懂。训练数据我们使用的是http://mattmahoney.net/dc/enwik8.zip数据,里面是维基百科的数据。

def maybe_download(filename, url):

"""Download a file if not present, and make sure it's the right size."""

if not os.path.exists(filename):

filename, _ = urllib.urlretrieve(url + filename, filename)

return filename

# 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

def collect_data(vocabulary_size=10000):

url = 'http://mattmahoney.net/dc/'

filename = maybe_download('enwik8.zip', url)

vocabulary = read_data(filename)

print(vocabulary[:7])

data, count, dictionary, reverse_dictionary = build_dataset(vocabulary, vocabulary_size)

del vocabulary  # Hint to reduce memory.

return data, count, dictionary, reverse_dictionary

接下来是如何构建训练数据。构建训练数据主要包括统计词频,生成字典文件,并且根据字典文件给训练源数据中的单词进行编号等工作。我们生成的字典不可能包含所有的单词,一般我们按照单词频率由高到低排序,选择覆盖率大于95%的单词加入词典就可以了,因为词典越大,覆盖的场景越大,同时计算开销越大,这是一个均衡。下面的代码展示了这个过程,首先统计所有输入语料的词频,选出频率最高的10000个单词加入字典。同时在字典第一个位置插入一项“UNK"代表不能识别的单词,也就是未出现在字典的单词统一用UNK表示。然后给字典里每个词编号,并把源句子里每个词表示成在字典中的编号。我们可以根据每个词的编号查找WordEmbedding中的向量表示。

def build_dataset(words, n_words):

"""Process raw inputs into a dataset."""

count = [['UNK', -1]]

#  [['UNK', -1], ['i', 500], ['the', 498], ['man', 312], ...]

count.extend(collections.Counter(words).most_common(n_words - 1))

#  dictionary {'UNK':0, 'i':1, 'the': 2, 'man':3, ...}

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()))

# data: "I like cat" -> [1, 21, 124]

# count: [['UNK', 349], ['i', 500], ['the', 498], ['man', 312], ...]

# dictionary {'UNK':0, 'i':1, 'the': 2, 'man':3, ...}

# reversed_dictionary: {0:'UNK', 1:'i', 2:'the', 3:'man', ...}

return data, count, dictionary, reversed_dictionary

接下来我们看一下如何将源句子转换成训练过程的输入和输出,这一步是比较关键的。有两种业界常用的WordEmbedding生成方式,Continuous Bag Of Words (CBOW)方法和n-gram方法,我们采用n-gram方法。训练的目的是获得能够反映任意两个单词之间关系的单词向量表示,所以我们的输入到输出的映射也要翻译两个单词之间的关联。n-gram的思路是将所有的源句子按固定长度(比如128个单词)分割成很多batch。对于每个batch,从前往后每次选取长度为skip_window的窗口(我们设定skip_window=5)。对于窗口中的5个单词,我们生成两个source-target数据对,这两个source-target对的source都是窗口中间的单词,也就是第三个单词,然后从另外四个单词中随机选取两个作为两个target单词。然后窗口向后移动一个单词,每次向后移动一个位置获取下5个单词,一共循环64次,获取到64X2=128个source-target对,作为一个batch的训练数据。总的思路就是把某个单词和附近的单词组对,作为输入和输出。这里同一个source单词,会被映射到不同的target单词,这样理论上可以获取任意两个单词之间的关系。

比如对于句子"cat and dog play balls on the floor",第一个窗口就是“cat and dog play balls",生成的两个source-target对可能是下面中的任意两个:

dog -> cat

dog -> and

dog -> balls

dog -> play

第二个窗口是"and dog play balls on",生成的两个source-target对可能是下面中的任意两个:

play -> and

play -> balls

play -> dog

play -> on

下面是代码实现:

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  # span含义 -> [ skip_window input_word skip_window ]

# 初始化最大长度为span的双端队列,超过最大长度后再添加数据,会从另一端删除容不下的数据

# buffer: 1, 21, 124, 438, 11

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):  # 128 / 2

# target: 2

target = skip_window  # input word at the center of the buffer

# targets_to_avoid: [2]

targets_to_avoid = [skip_window]  # 需要忽略的词在当前span的位置

# 更新源单词为当前5个单词的中间单词

source_word = buffer[skip_window]

# 随机选择的5个span单词中除了源单词之外的4个单词中的两个

for j in range(num_skips):

while target in targets_to_avoid:  # 随机重新从5个词中选择一个尚未选择过的词

target = random.randint(0, span - 1)

targets_to_avoid.append(target)

# batch添加源单词

batch[i * num_skips + j] = source_word

# context添加目标单词,单词来自随机选择的5个span单词中除了源单词之外的4个单词中的两个

context[i * num_skips + j, 0] = buffer[target]

# 往双端队列中添加下一个单词,双端队列会自动将容不下的数据从另一端删除

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

接下来是构建神经网络的过程,我们构建了一个包含一个隐藏层的神经网络,该隐藏层包含300个节点,这个数量和我们要构造的WordEmbedding维度一致。

with graph.as_default():

# 定义输入输出

train_sources = tf.placeholder(tf.int32, shape=[batch_size])

train_targets = tf.placeholder(tf.int32, shape=[batch_size, 1])

valid_dataset = tf.constant(valid_examples, dtype=tf.int32)

# 初始化embeddings矩阵,这个就是经过多步训练后最终我们需要的embedding

embeddings = tf.Variable(tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0))

# 将输入序列转换成embedding表示, [batch_size, embedding_size]

embed = tf.nn.embedding_lookup(embeddings, train_sources)

# 初始化权重

weights = tf.Variable(tf.truncated_normal([embedding_size, vocabulary_size], stddev=1.0 / math.sqrt(embedding_size)))

biases = tf.Variable(tf.zeros([vocabulary_size]))

# 隐藏层输出结果的计算, [batch_size, vocabulary_size]

hidden_out = tf.transpose(tf.matmul(tf.transpose(weights), tf.transpose(embed))) + biases

# 将label结果转换成one-hot表示, [batch_size, 1] -> [batch_size, vocabulary_size]

train_one_hot = tf.one_hot(train_targets, vocabulary_size)

# 根据隐藏层输出结果和标记结果,计算交叉熵

cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=hidden_out, labels=train_one_hot))

# 随机梯度下降进行一步反向传递

optimizer = tf.train.GradientDescentOptimizer(1.0).minimize(cross_entropy)

# 计算验证数据集中的单词和字典表里所有单词的相似度,并在validate过程输出相似度最高的几个单词

norm = tf.sqrt(tf.reduce_sum(tf.square(embeddings), 1, keep_dims=True))

normalized_embeddings = embeddings / norm

valid_embeddings = tf.nn.embedding_lookup(normalized_embeddings, valid_dataset)

similarity = tf.matmul(valid_embeddings, normalized_embeddings, transpose_b=True)

# 参数初始化赋值

init = tf.global_variables_initializer()

我们首先随机初始化embeddings矩阵,通过tf.nn.embedding_lookup函数将输入序列转换成WordEmbedding表示作为隐藏层的输入。初始化weights和biases,计算隐藏层的输出。然后计算输出和target结果的交叉熵,使用GradientDescentOptimizer完成一次反向传递,更新可训练的参数,包括embeddings变量。在Validate过程中,对测试数据集中的单词,利用embeddings矩阵计算测试单词和所有其他单词的相似度,输出相似度最高的几个单词,看看它们相关性如何,作为一种验证方式。

通过这个神经网络,就可以完成WordEmbedding的训练,继而应用于其他NLP的任务。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容