提交 da62210f 编写于 作者: A Aston Zhang

before skipgram

上级 c5c85c86
......@@ -4,7 +4,7 @@
首先让我们导入实验所需的包或模块。
```{.python .input n=263}
```{.python .input n=1}
import sys
sys.path.insert(0, '..')
......@@ -23,7 +23,7 @@ import zipfile
Penn Tree Bank(PTB)是一个常用的小型语料库 [1]。它采样自华尔街日报的文章,包括训练集、验证集和测试集。我们将在PTB的训练集上训练词嵌入模型。该数据集的每一行作为一个句子。句子中的每个词由空格隔开。
```{.python .input n=264}
```{.python .input n=2}
with zipfile.ZipFile('../data/ptb.zip', 'r') as zin:
zin.extractall('../data/')
......@@ -37,7 +37,7 @@ with open('../data/ptb/ptb.train.txt', 'r') as f:
对于数据集的前三个句子,打印每个句子的词数和前五个词。这个数据集中句尾符为“<eos>”,生僻词全用“<unk>”表示,数字则被替换成了“N”。
```{.python .input n=265}
```{.python .input n=3}
for st in raw_dataset[:3]:
print('# tokens:', len(st), st[:5])
```
......@@ -46,7 +46,7 @@ for st in raw_dataset[:3]:
为了计算简单,我们只保留在数据集中至少出现5次的词。
```{.python .input n=266}
```{.python .input n=4}
# tk 是 token 的在循环中的缩写。
counter = collections.Counter([tk for st in raw_dataset for tk in st])
counter = dict(filter(lambda x: x[1] >= 5, counter.items()))
......@@ -54,7 +54,7 @@ counter = dict(filter(lambda x: x[1] >= 5, counter.items()))
然后将词映射到整数索引。
```{.python .input n=267}
```{.python .input n=5}
idx_to_token = [tk for tk, _ in counter.items()]
token_to_idx = {tk: idx for idx, tk in enumerate(idx_to_token)}
dataset = [[token_to_idx[tk] for tk in st if tk in token_to_idx]
......@@ -71,7 +71,7 @@ $$ \mathbb{P}(w_i) = \max\left(1 - \sqrt{\frac{t}{f(w_i)}}, 0\right),$$
其中 $f(w_i)$ 是数据集中词$w_i$的个数与总词数之比,常数$t$是一个超参数(实验中设为$10^{-4}$)。可见,只有当$f(w_i) > t$时,我们才有可能在二次采样中丢弃词$w_i$,并且越高频的词被丢弃的概率越大。
```{.python .input n=268}
```{.python .input n=6}
def discard(idx):
return random.uniform(0, 1) < 1 - math.sqrt(
1e-4 / counter[idx_to_token[idx]] * num_tokens)
......@@ -82,7 +82,7 @@ subsampled_dataset = [[tk for tk in st if not discard(tk)] for st in dataset]
可以看到,二次采样后我们去掉了一半左右的词。下面比较一个词在二次采样前后出现在数据集中的次数。可见高频词“the”的采样率不足1/20。
```{.python .input n=269}
```{.python .input n=7}
def compare_counts(token):
return '# %s: before=%d, after=%d' % (token, sum(
[st.count(token_to_idx[token]) for st in dataset]), sum(
......@@ -93,7 +93,7 @@ compare_counts('the')
但低频词“join”则完整地保留了下来。
```{.python .input n=270}
```{.python .input n=8}
compare_counts('join')
```
......@@ -101,7 +101,7 @@ compare_counts('join')
我们将与中心词距离不超过背景窗口大小的词作为它的背景词。下面定义函数提取出所有中心词和它们的背景词。它每次在整数1和`max_window_size`(最大背景窗口)之间均匀随机采样一个整数作为背景窗口大小。
```{.python .input n=271}
```{.python .input n=9}
def get_centers_and_contexts(dataset, max_window_size):
centers, contexts = [], []
for st in dataset:
......@@ -119,7 +119,7 @@ def get_centers_and_contexts(dataset, max_window_size):
下面我们创建一个人工数据集,其中含有词数分别为7和3的两个句子。设最大背景窗口为2,打印所有中心词和它们的背景词。
```{.python .input n=272}
```{.python .input n=10}
tiny_dataset = [list(range(7)), list(range(7, 10))]
print('dataset', tiny_dataset)
for center, context in zip(*get_centers_and_contexts(tiny_dataset, 2)):
......@@ -128,7 +128,7 @@ for center, context in zip(*get_centers_and_contexts(tiny_dataset, 2)):
实验中,我们设最大背景窗口大小为5。下面提取数据集中所有的中心词及其背景词。
```{.python .input n=273}
```{.python .input n=11}
all_centers, all_contexts = get_centers_and_contexts(subsampled_dataset, 5)
```
......@@ -136,7 +136,7 @@ all_centers, all_contexts = get_centers_and_contexts(subsampled_dataset, 5)
我们使用负采样来进行近似训练。对于一对中心词和背景词,我们随机采样$K$个噪音词(实验中设$K=5$)。根据word2vec论文的建议,噪音词采样概率$\mathbb{P}(w)$设为$w$词频与总词频之比的0.75次方 [2]。
```{.python .input n=274}
```{.python .input n=12}
def get_negatives(all_contexts, sampling_weights, K):
all_negatives, neg_candidates, i = [], [], 0
population = list(range(len(sampling_weights)))
......@@ -159,40 +159,40 @@ sampling_weights = [counter[w]**0.75 for w in idx_to_token]
all_negatives = get_negatives(all_contexts, sampling_weights, 5)
```
## 数据读取
## 读取数据
我们从数据集中提取了所有中心词`all_centers`,和每个中心词对应的背景词`all_contexts`和噪音词`all_negatives`。接下来我们使用随机小批量来读取它们。
我们从数据集中提取所有中心词`all_centers`,以及每个中心词对应的背景词`all_contexts`和噪音词`all_negatives`。我们将通过随机小批量来读取它们。
在一个小批量数据中,第$i$个样本包括一个中心词和它对应的$n_i$个背景词和$m_i$个噪音词。但由于每个样本的背景窗口大小可能不一样,这样背景词与噪音词数量和$n_i+m_i$也会不同。在构造小批量时,我们将每个样本的背景词和噪音词连结在一起,并将长度固定为$l=\max_i n_i+m_i$,也就是形状为(批量大小,$l$)。如果某个样本的长度不够,我们添加0来补齐长度。同时我们构造同样形状的标号:1表示背景词,0表示其他。以及同样形状的掩码,1表示背景词或噪音词,0表示填充
在一个小批量数据中,第$i$个样本包括一个中心词以及它所对应的$n_i$个背景词和$m_i$个噪音词。由于每个样本的背景窗口大小可能不一样,其中背景词与噪音词个数之和$n_i+m_i$也会不同。在构造小批量时,我们将每个样本的背景词和噪音词连结在一起,并添加填充项0直至连结后的长度相同,即长度均为$\max_i n_i+m_i$(`max_len`)。为了避免填充项对损失函数计算的影响,我们构造了掩码变量`masks`,其每一个元素分别与连结后的背景词和噪音词`contexts_negatives`中的元素一一对应。当变量`contexts_negatives`中的某个元素为填充项时,相同位置的掩码变量`masks`中的元素取0,否则取1。为了区分正例和负例,我们还需要将`contexts_negatives`变量中的背景词和噪音词区分开来。依据掩码变量的构造思路,我们只需创建与`contexts_negatives`变量形状相同的标签变量`labels`,并将与背景词(正例)对应的元素设1,其余清0
下面实现给定一个长度为批量大小的序列,其中每个元素为(中心词,背景词,噪音词),返回我们需要的小批量数据格式
下面我们将实现这个小批量读取函数`batchify`。它的小批量输入`data`是一个长度为批量大小的列表,其中每个元素分别包含中心词`center`、背景词`context`和噪音词`negative`。该函数返回的小批量数据符合我们所需要的格式,例如包含了掩码变量
```{.python .input n=277}
```{.python .input n=13}
def batchify(data):
l = max(len(c) + len(n) for _, c, n in data)
max_len = max(len(c) + len(n) for _, c, n in data)
centers, contexts_negatives, masks, labels = [], [], [], []
for center, context, negative in data:
cur_l = len(context) + len(negative)
cur_len = len(context) + len(negative)
centers += [center]
contexts_negatives += [context + negative + [0] * (l - cur_l)]
masks += [[1] * cur_l + [0] * (l - cur_l)]
labels += [[1] * len(context) + [0] * (l - len(context))]
contexts_negatives += [context + negative + [0] * (max_len - cur_len)]
masks += [[1] * cur_len + [0] * (max_len - cur_len)]
labels += [[1] * len(context) + [0] * (max_len - len(context))]
return (nd.array(centers).reshape((-1, 1)), nd.array(contexts_negatives),
nd.array(masks), nd.array(labels))
```
下面创建小批量读取迭代器,并打印读取的第一个批量中的数据形状。
我们用刚刚定义的`batchify`函数指定`DataLoader`实例中小批量的读取方式。然后打印读取的第一个批量中各个变量的形状。
```{.python .input n=278}
```{.python .input n=14}
batch_size = 512
num_workers = 0 if sys.platform.startswith('win32') else 4
dataset = gdata.ArrayDataset(all_centers, all_contexts, all_negatives)
data_iter = gdata.DataLoader(dataset, batch_size, shuffle=True,
batchify_fn=batchify, num_workers=num_workers)
for batch in data_iter:
for name, data in zip(['centers', 'contexts_negatives',
'masks', 'labels'], batch):
print(name, ':', data.shape)
for name, data in zip(['centers', 'contexts_negatives', 'masks',
'labels'], batch):
print(name, 'shape:', data.shape)
break
```
......@@ -204,7 +204,7 @@ for batch in data_iter:
获取词嵌入的层被称为嵌入层。嵌入层的权重是一个(词典大小,嵌入向量长度$d$)的矩阵。输入一个词的索引$i$,它返回权重的第$i$行作为它的词嵌入向量。在Gluon中可以通过`nn.Embedding`类来得到嵌入层。
```{.python .input n=188}
```{.python .input n=15}
embed = nn.Embedding(input_dim=20, output_dim=2)
embed.initialize()
embed.weight
......@@ -212,7 +212,7 @@ embed.weight
嵌入层输入为词索引。假设输入形状为(批量大小,$l$),那么输出的形状为(批量大小,$l$,$d$),最后一个维度用来放置嵌入向量。
```{.python .input n=184}
```{.python .input n=16}
x = nd.array([[1,2,3]])
embed(x)
```
......@@ -221,7 +221,7 @@ embed(x)
我们可以将背景词向量和噪音词向量合并起来,然后使用一次矩阵乘法来计算中心词向量和它们的内积$\boldsymbol{v}_{c}^\top\left[\boldsymbol{u}_{o_1},\ldots,\boldsymbol{u}_{o_{n+m}}\right]$。我们需要对小批量里的每个中心词逐一做此运算,虽然可以用for循环来实现,但使用`batch_dot`(用$\text{bd}$表示)通常可以得到更好的性能。假设$\boldsymbol{X}=\left[\boldsymbol{X}_1,\ldots,\boldsymbol{X}_n\right]$且$\boldsymbol{Y}=\left[\boldsymbol{Y}_1,\ldots,\boldsymbol{Y}_n\right]$,如果$\boldsymbol{Z}=\text{bd}(\boldsymbol{X},\boldsymbol{Y})$,那么$\boldsymbol{Z}=\left[\boldsymbol{X}_1\boldsymbol{Y}_1,\ldots,\boldsymbol{X}_n\boldsymbol{Y}_n\right]$。
```{.python .input n=259}
```{.python .input n=17}
X = nd.ones((2, 3, 4))
Y = nd.ones((2, 4, 6))
nd.batch_dot(X, Y).shape
......@@ -231,7 +231,7 @@ nd.batch_dot(X, Y).shape
在前向计算中,跳字模型的输入是形状为(批量大小,1)的中心词索引,形状为(批量大小,$n+m$)的背景词和噪音词索引,以及对应的嵌入层。输出形状为(批量大小,1,$n+m$),每个元素是一对嵌入向量的内积。
```{.python .input}
```{.python .input n=18}
def skip_gram(center, contexts_and_negatives, embed_v, embed_u):
v = embed_v(center)
u = embed_u(contexts_and_negatives)
......@@ -253,7 +253,7 @@ $$\sum_{k=1}^{n+m}y_k\log\,\sigma(\boldsymbol{u}_{o_k}^\top\boldsymbol{v}_c) + (
从而我们可以直接使用Gluon提供的`SigmoidBinaryCrossEntropyLoss`
```{.python .input}
```{.python .input n=19}
loss = gloss.SigmoidBinaryCrossEntropyLoss()
```
......@@ -261,7 +261,7 @@ loss = gloss.SigmoidBinaryCrossEntropyLoss()
我们构造中心词和背景词对应的嵌入层,并将超参数嵌入向量长度设置成100。
```{.python .input}
```{.python .input n=20}
embed_size = 100
net = nn.Sequential()
net.add(nn.Embedding(input_dim=len(idx_to_token), output_dim=embed_size),
......@@ -272,7 +272,7 @@ net.add(nn.Embedding(input_dim=len(idx_to_token), output_dim=embed_size),
下面定义训练函数。注意由于填充的关系,在计算损失处跟之前的训练函数略微不同。
```{.python .input n=228}
```{.python .input n=21}
def train(net, lr, num_epochs):
ctx = gb.try_gpu()
net.initialize(ctx=ctx, force_reinit=True)
......@@ -299,7 +299,7 @@ def train(net, lr, num_epochs):
现在我们可以训练使用负采样的跳字模型了。可以看到,使用训练得到的词嵌入模型时,与词“chip”语义最接近的词大多与芯片有关。
```{.python .input n=229}
```{.python .input n=22}
train(net, 0.005, 8)
```
......@@ -307,7 +307,7 @@ train(net, 0.005, 8)
当我们训练好词嵌入模型后,我们可以根据两个词向量的余弦相似度表示词与词之间在语义上的相似度。
```{.python .input n=235}
```{.python .input n=23}
def get_similar_tokens(query_token, k, embed):
W = embed.weight.data()
x = W[token_to_idx[query_token]]
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册