深度学习基础(二)

文本预处理

文本是一类序列数据,一篇文章可以看作是字符或单词的序列,本节将介绍文本数据的常见预处理步骤,预处理通常包括四个步骤:

  1. 读入文本
  2. 分词
  3. 建立字典,将每个词映射到一个唯一的索引(index)
  4. 将文本从词的序列转换为索引的序列,方便输入模型

正则表达式相关:正则表达式学习
自定义文本预处理示例:

import collections
import re
#我们用一部英文小说,即H. G. Well的Time Machine,作为示例,展示文本预处理的具体过程。
def read_time_machine():
    with open('/home/kesci/input/timemachine7163/timemachine.txt', 'r') as f:
        lines = [re.sub('[^a-z]+', ' ', line.strip().lower()) for line in f]
        # lines =[ line.strip().lower() for line in f]
    return lines
"""
re.sub(pattern, repl, string, count=0, flags=0)
参数:
pattern : 正则中的模式字符串。
repl : 替换的字符串,也可为一个函数。
string : 要被查找替换的原始字符串。
count : 模式匹配后替换的最大次数,默认 0 表示替换所有的匹配
"""
###对每个句子进行分词,也就是将一个句子划分成若干个词(token),转换为一个词的序列。
def tokenize(sentences, token='word'):
    """Split sentences into word or char tokens"""
    if token == 'word':
        return [sentence.split(' ') for sentence in sentences]
    elif token == 'char':
        return [list(sentence) for sentence in sentences]
    else:
        print('ERROR: unkown token type '+token)
#为了方便模型处理,我们需要将字符串转换为数字。
#因此我们需要先构建一个字典(vocabulary),将每个词映射到一个唯一的索引编号。
class Vocab(object):
    def __init__(self, tokens, min_freq=0, use_special_tokens=False):
        counter = count_corpus(tokens)  # : 
        self.token_freqs = list(counter.items())
        self.idx_to_token = []
        if use_special_tokens:
            # padding, begin of sentence, end of sentence, unknown
            self.pad, self.bos, self.eos, self.unk = (0, 1, 2, 3)
            self.idx_to_token += ['', '', '', '']
        else:
            self.unk = 0
            self.idx_to_token += ['']
        self.idx_to_token += [token for token, freq in self.token_freqs
                        if freq >= min_freq and token not in self.idx_to_token]
        self.token_to_idx = dict()
        for idx, token in enumerate(self.idx_to_token):
            self.token_to_idx[token] = idx

    def __len__(self):
        return len(self.idx_to_token)

    def __getitem__(self, tokens):
        if not isinstance(tokens, (list, tuple)):
            return self.token_to_idx.get(tokens, self.unk)
        return [self.__getitem__(token) for token in tokens]

    def to_tokens(self, indices):
        if not isinstance(indices, (list, tuple)):
            return self.idx_to_token[indices]
        return [self.idx_to_token[index] for index in indices]

def count_corpus(sentences):
    tokens = [tk for st in sentences for tk in st]
    return collections.Counter(tokens)  # 返回一个字典,记录每个词的出现次数
    
#读取文本数据
lines = read_time_machine()
#分词
tokens = tokenize(lines)
#建立字典,替换索引
vocab = Vocab(tokens)
print(list(vocab.token_to_idx.items())[0:10])
for i in range(8, 10):
    print('words:', tokens[i])
    print('indices:', vocab[tokens[i]])

用现有工具进行分词

我们前面介绍的分词方式非常简单,它至少有以下几个缺点:

  1. 标点符号通常可以提供语义信息,但是我们的方法直接将其丢弃了
  2. 类似“shouldn’t", "doesn’t"这样的词会被错误地处理
  3. 类似"Mr.", "Dr."这样的词会被错误地处理

我们可以通过引入更复杂的规则来解决这些问题,但是事实上,有一些现有的工具可以很好地进行分词,我们在这里简单介绍其中的两个:spaCyNLTK

spaCy

import spacy

# Load English tokenizer, tagger, parser, NER and word vectors
nlp = spacy.load("en_core_web_sm")

# Process whole documents
text = ("When Mr. Sebastian Thrun started working on self-driving cars at "
        "Google in 2007, few people outside of the company took him "
        "seriously. “I can tell you very senior CEOs of major American "
        "car companies would shake my hand and turn away because I wasn’t "
        "worth talking to,” said Thrun, in an interview with Recode earlier "
        "this week.")
doc = nlp(text)

# Analyze syntax
print("Noun phrases:", [chunk.text for chunk in doc.noun_chunks])
print("Verbs:", [token.lemma_ for token in doc if token.pos_ == "VERB"])
for entity in doc.ents:
    print(entity.text, entity.label_)

NLTK

from nltk.tokenize import word_tokenize
print(word_tokenize(text))

语言模型

一段自然语言文本可以看作是一个离散时间序列,给定一个长度为 T T T的词的序列 w 1 , w 2 , … , w T w_1, w_2, \ldots, w_T w1,w2,,wT,语言模型的目标就是评估该序列是否合理,即计算该序列的概率:
P ( w 1 , w 2 , … , w T ) . P(w_1, w_2, \ldots, w_T). P(w1,w2,,wT).

假设序列 w 1 , w 2 , … , w T w_1, w_2, \ldots, w_T w1,w2,,wT中的每个词是依次生成的,我们有
P ( w 1 , w 2 , … , w T ) = ∏ t = 1 T P ( w t ∣ w 1 , … , w t − 1 ) = P ( w 1 ) P ( w 2 ∣ w 1 ) ⋯ P ( w T ∣ w 1 w 2 ⋯ w T − 1 ) P({w_1},{w_2}, \ldots ,{w_T}) = \prod\limits_{t = 1}^T P ({w_t}\mid {w_1}, \ldots ,{w_{t - 1}}) = P({w_1})P({w_2}\mid {w_1}) \cdots P({w_T}\mid {w_1}{w_2} \cdots {w_{T - 1}}) P(w1,w2,,wT)=t=1TP(wtw1,,wt1)=P(w1)P(w2w1)P(wTw1w2wT1)
例如,一段含有4个词的文本序列的概率
P ( w 1 , w 2 , w 3 , w 4 ) = P ( w 1 ) P ( w 2 ∣ w 1 ) P ( w 3 ∣ w 1 , w 2 ) P ( w 4 ∣ w 1 , w 2 , w 3 ) . P(w_1, w_2, w_3, w_4) = P(w_1) P(w_2 \mid w_1) P(w_3 \mid w_1, w_2) P(w_4 \mid w_1, w_2, w_3). P(w1,w2,w3,w4)=P(w1)P(w2w1)P(w3w1,w2)P(w4w1,w2,w3).
语言模型的参数就是词的概率以及给定前几个词情况下的条件概率。设训练数据集为一个大型文本语料库,如维基百科的所有条目,词的概率可以通过该词在训练数据集中的相对词频来计算,例如, w 1 w_1 w1的概率可以计算为:
P ^ ( w 1 ) = n ( w 1 ) n \hat P(w_1) = \frac{n(w_1)}{n} P^(w1)=nn(w1)
其中 n ( w 1 ) n(w_1) n(w1)为语料库中以 w 1 w_1 w1作为第一个词的文本的数量, n n n为语料库中文本的总数量。
类似的,给定 w 1 w_1 w1情况下, w 2 w_2 w2的条件概率可以计算为:
P ^ ( w 2 ∣ w 1 ) = n ( w 1 , w 2 ) n ( w 1 ) \hat P(w_2 \mid w_1) = \frac{n(w_1, w_2)}{n(w_1)} P^(w2w1)=n(w1)n(w1,w2)
其中 n ( w 1 , w 2 ) n(w_1, w_2) n(w1,w2)为语料库中以 w 1 w_1 w1作为第一个词, w 2 w_2 w2作为第二个词的文本的数量。
缺陷: 参数空间过大, 数据稀疏

时序数据的采样

在训练中我们需要每次随机读取小批量样本和标签。与之前章节的实验数据不同的是,时序数据的一个样本通常包含连续的字符。假设时间步数为5,样本序列为5个字符,即“想”“要”“有”“直”“升”。该样本的标签序列为这些字符分别在训练集中的下一个字符,即“要”“有”“直”“升”“机”,即 X X X=“想要有直升”, Y Y Y=“要有直升机”。

现在我们考虑序列“想要有直升机,想要和你飞到宇宙去”,如果时间步数为5,有以下可能的样本和标签:

  • X X X:“想要有直升”, Y Y Y:“要有直升机”
  • X X X:“要有直升机”, Y Y Y:“有直升机,”
  • X X X:“有直升机,”, Y Y Y:“直升机,想”
  • X X X:“要和你飞到”, Y Y Y:“和你飞到宇”
  • X X X:“和你飞到宇”, Y Y Y:“你飞到宇宙”
  • X X X:“你飞到宇宙”, Y Y Y:“飞到宇宙去”

可以看到,如果序列的长度为 T T T,时间步数为 n n n,那么一共有 T − n T-n Tn个合法的样本,但是这些样本有大量的重合,我们通常采用更加高效的采样方式。我们有两种方式对时序数据进行采样,分别是随机采样和相邻采样。
随机采样
下面的代码每次从数据里随机采样一个小批量。其中批量大小batch_size是每个小批量的样本数,num_steps是每个样本所包含的时间步数。
在随机采样中,每个样本是原始序列上任意截取的一段序列,相邻的两个随机小批量在原始序列上的位置不一定相毗邻。

import torch
import random
def data_iter_random(corpus_indices, batch_size, num_steps, device=None):
    # 减1是因为对于长度为n的序列,X最多只有包含其中的前n - 1个字符
    num_examples = (len(corpus_indices) - 1) // num_steps  # 下取整,得到不重叠情况下的样本个数
    example_indices = [i * num_steps for i in range(num_examples)]  # 每个样本的第一个字符在corpus_indices中的下标
    random.shuffle(example_indices)

    def _data(i):
        # 返回从i开始的长为num_steps的序列
        return corpus_indices[i: i + num_steps]
    if device is None:
        device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    
    for i in range(0, num_examples, batch_size):
        # 每次选出batch_size个随机样本
        batch_indices = example_indices[i: i + batch_size]  # 当前batch的各个样本的首字符的下标
        X = [_data(j) for j in batch_indices]
        Y = [_data(j + 1) for j in batch_indices]
        yield torch.tensor(X, device=device), torch.tensor(Y, device=device)

相邻采样
在相邻采样中,相邻的两个随机小批量在原始序列上的位置相毗邻。

def data_iter_consecutive(corpus_indices, batch_size, num_steps, device=None):
    if device is None:
        device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    corpus_len = len(corpus_indices) // batch_size * batch_size  # 保留下来的序列的长度
    corpus_indices = corpus_indices[: corpus_len]  # 仅保留前corpus_len个字符
    indices = torch.tensor(corpus_indices, device=device)
    indices = indices.view(batch_size, -1)  # resize成(batch_size, )
    batch_num = (indices.shape[1] - 1) // num_steps
    for i in range(batch_num):
        i = i * num_steps
        X = indices[:, i: i + num_steps]
        Y = indices[:, i + 1: i + num_steps + 1]
        yield X, Y

循环神经网络

本节介绍循环神经网络,下图展示了如何基于循环神经网络实现语言模型。我们的目的是基于当前的输入与过去的输入序列,预测序列的下一个字符。循环神经网络引入一个隐藏变量 H H H,用 H t H_{t} Ht表示 H H H在时间步 t t t的值。 H t H_{t} Ht的计算基于 X t X_{t} Xt H t − 1 H_{t-1} Ht1,可以认为 H t H_{t} Ht记录了到当前字符为止的序列信息,利用 H t H_{t} Ht对序列的下一个字符进行预测。
Image Name

循环神经网络的构造

我们先看循环神经网络的具体构造。假设 X t ∈ R n × d \boldsymbol{X}_t \in \mathbb{R}^{n \times d} XtRn×d是时间步 t t t的小批量输入, H t ∈ R n × h \boldsymbol{H}_t \in \mathbb{R}^{n \times h} HtRn×h是该时间步的隐藏变量,则:

H t = ϕ ( X t W x h + H t − 1 W h h + b h ) . \boldsymbol{H}_t = \phi(\boldsymbol{X}_t \boldsymbol{W}_{xh} + \boldsymbol{H}_{t-1} \boldsymbol{W}_{hh} + \boldsymbol{b}_h). Ht=ϕ(XtWxh+Ht1Whh+bh).

其中, W x h ∈ R d × h \boldsymbol{W}_{xh} \in \mathbb{R}^{d \times h} WxhRd×h W h h ∈ R h × h \boldsymbol{W}_{hh} \in \mathbb{R}^{h \times h} WhhRh×h b h ∈ R 1 × h \boldsymbol{b}_{h} \in \mathbb{R}^{1 \times h} bhR1×h ϕ \phi ϕ函数是非线性激活函数。由于引入了 H t − 1 W h h \boldsymbol{H}_{t-1} \boldsymbol{W}_{hh} Ht1Whh H t H_{t} Ht能够捕捉截至当前时间步的序列的历史信息,就像是神经网络当前时间步的状态或记忆一样。由于 H t H_{t} Ht的计算基于 H t − 1 H_{t-1} Ht1,上式的计算是循环的,使用循环计算的网络即循环神经网络(recurrent neural network)。

在时间步 t t t,输出层的输出为:

O t = H t W h q + b q . \boldsymbol{O}_t = \boldsymbol{H}_t \boldsymbol{W}_{hq} + \boldsymbol{b}_q. Ot=HtWhq+bq.

其中 W h q ∈ R h × q \boldsymbol{W}_{hq} \in \mathbb{R}^{h \times q} WhqRh×q b q ∈ R 1 × q \boldsymbol{b}_q \in \mathbb{R}^{1 \times q} bqR1×q

裁剪梯度

循环神经网络中较容易出现梯度衰减或梯度爆炸,这会导致网络几乎无法训练。裁剪梯度(clip gradient)是一种应对梯度爆炸的方法。假设我们把所有模型参数的梯度拼接成一个向量 g \boldsymbol{g} g,并设裁剪的阈值是 θ \theta θ。裁剪后的梯度。
min ⁡ ( θ ∥ g ∥ , 1 ) g \min\left(\frac{\theta}{\|\boldsymbol{g}\|}, 1\right)\boldsymbol{g} min(gθ,1)g
L 2 L_2 L2范数不超过 θ \theta θ

困惑度

我们通常使用困惑度(perplexity)来评价语言模型的好坏。回忆一下交叉熵损失函数的定义。困惑度是对交叉熵损失函数做指数运算后得到的值。特别地,

  • 最佳情况下,模型总是把标签类别的概率预测为1,此时困惑度为1;
  • 最坏情况下,模型总是把标签类别的概率预测为0,此时困惑度为正无穷;
  • 基线情况下,模型总是预测所有类别的概率都相同,此时困惑度为类别个数。

显然,任何一个有效模型的困惑度必须小于类别个数。在本例中,困惑度必须小于词典大小vocab_size

定义模型训练函数

跟之前章节的模型训练函数相比,这里的模型训练函数有以下几点不同:

  1. 使用困惑度评价模型。
  2. 在迭代模型参数前裁剪梯度。
  3. 对时序数据采用不同采样方法将导致隐藏状态初始化的不同。
定义模型

我们使用Pytorch中的nn.RNN来构造循环神经网络。在本节中,我们主要关注nn.RNN的以下几个构造函数参数:

  • input_size - The number of expected features in the input x
  • hidden_size – The number of features in the hidden state h
  • nonlinearity – The non-linearity to use. Can be either ‘tanh’ or ‘relu’. Default: ‘tanh’
  • batch_first – If True, then the input and output tensors are provided as (batch_size, num_steps, input_size). Default: False

这里的batch_first决定了输入的形状,我们使用默认的参数False,对应的输入形状是 (num_steps, batch_size, input_size)。

forward函数的参数为:

  • input of shape (num_steps, batch_size, input_size): tensor containing the features of the input sequence.
  • h_0 of shape (num_layers * num_directions, batch_size, hidden_size): tensor containing the initial hidden state for each element in the batch. Defaults to zero if not provided. If the RNN is bidirectional, num_directions should be 2, else it should be 1.

forward函数的返回值是:

  • output of shape (num_steps, batch_size, num_directions * hidden_size): tensor containing the output features (h_t) from the last layer of the RNN, for each t.
  • h_n of shape (num_layers * num_directions, batch_size, hidden_size): tensor containing the hidden state for t = num_steps.

Yeah!
深度学习基础(三)
深度学习基础(一)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值