TensorFlow深度学习实战(13)——神经嵌入详解
0. 前言
神经嵌入 (Neural Embedding
) 是一种通过神经网络模型将离散的符号(如词语、字符、图像等)映射到低维连续向量空间中的技术。它属于更广泛的嵌入 (Embedding
) 技术范畴,在深度学习中起着关键作用。神经嵌入通过在神经网络训练过程中学习到的向量表示,捕捉了输入数据的潜在特征和语义信息。
1. 神经嵌入简介
自 Word2Vec 和 GloVe 提出以来,词嵌入技术已经取得了多方向的发展。其中一种方向是将词嵌入应用于非词汇环境,也称为神经嵌入 (Neural Embedding
) 。我们知道,词嵌入利用了分布假设,即出现在相似上下文中的词通常具有相似的含义,其中上下文通常是围绕目标词的一个固定大小(单词数量)的窗口。
神经嵌入的核心思想与之相似,即出现在相似上下文中的实体通常彼此密切相关。构建这些上下文的方式通常依赖于具体情况。接下来,我们将介绍两种基础且通用的技术,能够应用于多种用例。
1.1 Item2Vec
Item2Vec
嵌入模型最早由 Barkan
和 Koenigstein
提出,用于协同过滤 (collaborative filtering
),即根据具有类似购买历史的用户向目标用户推荐商品。使用商品作为“词”,用户随时间购买的商品集合(即商品序列)作为“句子”,从中推导出“词上下文”。
例如,考虑在超市向购物者推荐商品的问题。假设超市销售 5,000
种商品,因此每种商品可以表示为大小为 5,000
的稀疏独热编码向量,每个用户由其购物车表示,即一系列独热编码向量。应用类似于 Word2Vec
中的上下文窗口,可以训练一个 skip-gram
模型来预测可能的商品对。学习到的嵌入模型将商品映射到一个稠密的低维空间,其中相似的商品彼此靠近,可以用于进行相似商品的推荐。
1.2 node2vec
node2vec
嵌入模型由 Grover
和 Leskovec
提出,作为一种可扩展的方式学习图中节点特征。通过在图上执行大量固定长度的随机游走来学习图结构的嵌入。节点是视为“单词”,随机游走是从中派生“词上下文”的“句子”。
2. 数据集与模型分析
为了说明创建自定义神经嵌入的简便性,我们将利用词语之间的共现关系,生成类似于 node2vec
的模型或者更准确地说是一个基于图的嵌入,即 DeepWalk
,用于分析 1987-2015
年间 NeurIPS
会议上的论文。
数据集是一个 11,463×5,812
的词频矩阵,其中行代表词语,列代表会议论文。我们将利用此数据构建论文的图,其中两篇论文之间的边表示它们之间共同出现的词语。
node2vec
和 DeepWalk
都假设图是无向且无权重的。构建的图是无向的,因为两篇论文之间的关系是双向的。但是,边可以根据两篇文档之间词语共现的数量进行加权。在本节中,我们将共现数量大于 0
的情况视为有效的无权重边。
3. 实现神经嵌入
(1) 首先,导入所需库:
import gensim
import logging
import numpy as np
import os
import shutil
import tensorflow as tf
from scipy.sparse import csr_matrix
# from scipy.stats import spearmanr
from sklearn.metrics.pairwise import cosine_similarity
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
(2) 从 UCI
下载数据,并将其转换为稀疏的词-文档矩阵 TD
,然后通过将词-文档矩阵的转置与自身相乘来构建文档-文档矩阵 E
。构建的图通过文档-文档矩阵表示为邻接矩阵或边矩阵。由于每个元素代表两个文档之间的相似性,我们通过将矩阵E中的非零元素设置为 1
来对矩阵 E
进行二值化处理:
DATA_DIR = "./data"
UCI_DATA_URL = "https://archive.ics.uci.edu/ml/machine-learning-databases/00371/NIPS_1987-2015.csv"
def download_and_read(url):
local_file = url.split('/')[-1]
p = tf.keras.utils.get_file(local_file, url, cache_dir=".")
row_ids, col_ids, data = [], [], []
rid = 0
f = open(p, "r")
for line in f:
line = line.strip()
if line.startswith("\"\","):
# header
continue
if rid % 100 == 0:
print("{:d} rows read".format(rid))
# compute non-zero elements for current row
counts = np.array([int(x) for x in line.split(',')[1:]])
nz_col_ids = np.nonzero(counts)[0]
nz_data = counts[nz_col_ids]
nz_row_ids = np.repeat(rid, len(nz_col_ids))
rid += 1
# add data to big lists
row_ids.extend(nz_row_ids.tolist())
col_ids.extend(nz_col_ids.tolist())
data.extend(nz_data.tolist())
print("{:d} rows read, COMPLETE".format(rid))
f.close()
TD = csr_matrix((
np.array(data), (
np.array(row_ids), np.array(col_ids)
)
),
shape=(rid, counts.shape[0]))
return TD
# read data and convert to Term-Document matrix
TD = download_and_read(UCI_DATA_URL)
# compute undirected, unweighted edge matrix
E = TD.T * TD
# binarize
E[E > 0] = 1
print(E.shape)
(3) 获取了稀疏二值化的邻接矩阵E后,可以从每个顶点生成随机游走。从每个节点开始,构造 32
条最大长度为 40
个节点的随机游走,这些随机游走具有 0.15
的随机重启概率,这意味着对于任何节点,特定的随机游走有 15%
的概率在该节点结束。构建随机游走,并将其写入由 RANDOM_WALKS_FILE
指定的文件。为了说明输入的情况,查看该文件前 10
行,显示从节点 3274
开始的随机游走:
需要注意的是,这是一个非常缓慢的过程。
(4) RANDOM_WALKS_FILE
中的样本看起来像语言中的句子,其中词汇表是图中的所有节点 ID
。我们已经知道,词嵌入利用语言的结构生成词的分布式表示。DeepWalk
和 node2vec
等图嵌入方案也使用随机游走生成的“句子”执行相同的操作。这些嵌入可以捕捉图中节点之间的相似性:
NUM_WALKS_PER_VERTEX = 32
MAX_PATH_LENGTH = 40
RESTART_PROB = 0.15
RANDOM_WALKS_FILE = os.path.join(DATA_DIR, "random-walks.txt")
def construct_random_walks(E, n, alpha, l, ofile):
""" NOTE: takes a long time to do, consider using some parallelization
for larger problems.
"""
if os.path.exists(ofile):
print("random walks generated already, skipping")
return
f = open(ofile, "w")
for i in range(E.shape[0]): # for each vertex
if i % 100 == 0:
print("{:d} random walks generated from {:d} starting vertices"
.format(n * i, i))
if i <= 3273:
continue
for j in range(n): # construct n random walks
curr = i
walk = [curr]
target_nodes = np.nonzero(E[curr])[1]
for k in range(l): # each of max length l, restart prob alpha
# should we restart?
if np.random.random() < alpha and len(walk) > 5:
break
# choose one outgoing edge and append to walk
try:
curr = np.random.choice(target_nodes)
walk.append(curr)
target_nodes = np.nonzero(E[curr])[1]
except ValueError:
continue
f.write("{:s}\n".format(" ".join([str(x) for x in walk])))
print("{:d} random walks generated from {:d} starting vertices, COMPLETE".format(n * i, i))
f.close()
# construct random walks (caution: long process!)
construct_random_walks(E, NUM_WALKS_PER_VERTEX, RESTART_PROB, MAX_PATH_LENGTH, RANDOM_WALKS_FILE)
(5) 创建词嵌入模型。Gensim
包提供了一个简单的 API
,允许我们创建和训练 Word2Vec
模型,训练好的模型将序列化到 W2V_MODEL_FILE
指定的文件中。Documents
类允许我们流式处理大型输入文件以训练 Word2Vec
模型,避免出现内存问题。使用 skip-gram
模式训练 Word2Vec
模型,窗口大小为 10
,这意味着训练模型来预测给定中心顶点的最多五个相邻顶点。每个顶点的嵌入结果是一个大小为 128
的稠密向量:
W2V_MODEL_FILE = os.path.join(DATA_DIR, "w2v-neurips-papers.model")
class Documents(object):
def __init__(self, input_file):
self.input_file = input_file
def __iter__(self):
with open(self.input_file, "r") as f:
for i, line in enumerate(f):
if i % 1000 == 0:
if i % 1000 == 0:
logging.info("{:d} random walks extracted".format(i))
yield line.strip().split()
def train_word2vec_model(random_walks_file, model_file):
if os.path.exists(model_file):
print("Model file {:s} already present, skipping training"
.format(model_file))
return
docs = Documents(random_walks_file)
model = gensim.models.Word2Vec(
docs,
vector_size=128, # size of embedding vector
window=10, # window size
sg=1, # skip-gram model
min_count=2,
workers=4
)
model.train(
docs,
total_examples=model.corpus_count,
epochs=50)
model.save(model_file)
# train model
train_word2vec_model(RANDOM_WALKS_FILE, W2V_MODEL_FILE)
(6) 得到的 DeepWalk
模型实际上就是一个 Word2Vec
模型,因此在单词的上下文中可以用 Word2Vec
完成的任务,在顶点的上下文中也可以进行。使用该模型计算文档之间的相似性:
def evaluate_model(td_matrix, model_file, source_id):
model = gensim.models.Word2Vec.load(model_file).wv
most_similar = model.most_similar(str(source_id))
scores = [x[1] for x in most_similar]
target_ids = [int(x[0]) for x in most_similar]
# compare top 10 scores with cosine similarity between source and each target
X = np.repeat(td_matrix[source_id].todense(), 10, axis=0)
Y = td_matrix[target_ids].todense()
cosims = [cosine_similarity(np.asarray(X[i]), np.asarray(Y[i]))[0, 0] for i in range(10)]
for i in range(10):
print("{:d} {:s} {:.3f} {:.3f}".format(
source_id, str(target_ids[i]), cosims[i], scores[i]))
# evaluate
source_id = np.random.choice(E.shape[0])
evaluate_model(TD, W2V_MODEL_FILE, source_id)
输出结果如下所示。第一列和第二列是源和目标顶点的 ID
。第三列是对应于源和目标文档的词向量之间的余弦相似度,第四列是 Word2Vec
模型报告的相似度分数。可以看到,在 10
个文档对中,余弦相似度只得到 4
个相似度,但 Word2Vec
模型能够在嵌入空间中检测到潜在的相似性。这类似于在独热编码和稠密嵌入之间的行为:
小结
神经嵌入是将离散的符号映射到连续向量空间的强大技术,广泛应用于自然语言处理、推荐系统、计算机视觉和图数据分析等领域。通过神经网络训练,这些嵌入能够有效地捕捉数据中的潜在关系和语义信息,为各种任务提供了有效的特征表示。随着深度学习的发展,神经嵌入已经成为现代人工智能系统中不可或缺的一部分。
系列链接
TensorFlow深度学习实战(1)——神经网络与模型训练过程详解
TensorFlow深度学习实战(2)——使用TensorFlow构建神经网络
TensorFlow深度学习实战(3)——深度学习中常用激活函数详解
TensorFlow深度学习实战(4)——正则化技术详解
TensorFlow深度学习实战(5)——神经网络性能优化技术详解
TensorFlow深度学习实战(6)——回归分析详解
TensorFlow深度学习实战(7)——分类任务详解
TensorFlow深度学习实战(8)——卷积神经网络
TensorFlow深度学习实战(9)——构建VGG模型实现图像分类
TensorFlow深度学习实战(10)——迁移学习详解
TensorFlow深度学习实战(11)——风格迁移详解
TensorFlow深度学习实战(12)——词嵌入技术详解