Deep Learning with Sequence Data and Text
用RNNs的一些应用:
- Document classifier:文章分类或者情感分类
- Sequence-to-sequence learning:机器翻译
- Time-series forecasting: 给定前一天商品价格预测未来价格
Working with text data
对于文本的Deeplearning模型,就像其他的机器学习模型一样,需要将文本先转换为数值表示.这一过程称为向量化(vectorization).
向量化有多种不同的方式:
- 转换文本成单词,用向量表示每个单词
- 转换文本成字母,用向量表示每个字母
- 创建单词的n-gram,用向量表示它
显然,在将文本表示成向量形式前,需要先分割文本,将其分割成单词或字母这些更小的单元,称之为token.这个过程称之为tokenization.
Tokenization
Converting text into words
python自带英文文本的单词分割方法,直接调用split().主要是基于空格将文本切割为单词1
2a="Cao hong mei is my son"
a.split()
但中文单词之间不存在空格,这时候往往是将文本分成词语的形式,而不在只是一个单词.这里采用jieba库进行分词操作.
常见的中文分词算法:1.基于字符串匹配如正向最大匹配、逆向最大匹配2.基于统计以及机器学习的分词方法如HMM和CRF
几种jieba分词
- jieba.cut方法接收三个输入参数:需分词的字符串,cut_all来控制是否采用全模式,HMM参数用来控制是否采用HMM模型
- jieba.cut_for_search方法接受两个参数:需分词的字符串,是否使用HMM模型.该方法适用于搜索引擎构建倒排索引的分词,粒度比较细.
**注意:
- jieba.cut和jieba.cut_for_search返回的都是一个可迭代的generator,可以使用for循环获得分词后得到的每一个词语(unicode)
- jieba.lcut及jieba.lcut_for_search直接返回list
- jieba.Tokenizer(dictionary=DEFAULT_DICT)新建自定义分词器,可用于同时使用不同词典.
- 待分词的字符串可以是unicode或UTF-8字符串、GBK字符串.注意:不建议直接输入GBK字符串,可能无法预料地误解码为UTF-8.**
1
2
3jieba.lcut("曹红梅是我儿子",cut_all=True)
jieba.lcut("曹红梅是我儿子",cut_all=False)
jieba.lcut_for_search("曹红梅是我儿子")
Converting text into characters
一般只有英文需要将文本转化为字母,中文不需要.
很简单,只需要list操作即可.1
2a="Cao hong mei is my son"
list(a)
N-gram representation
这时候英文地n-gram需要用到nltk库,将分词后的单词序列作为ngrams的参数,并指定n的大小.1
2
3from nltk import ngrams
a="cao hong mei is my son"
list(ngrams(a.split(),2))
Vectorization
两种流行的向量化方法:1. one-hot encoding 2. word embedding
One-hot encoding
Onehot编码的前提是要对应词组已经有标签号,将对应词组映射到对应标签号.然后可以通过生成一个全零向量,在对应标签号位置置为1,则可表示该词组的onehot.
下面用了sklearn中的preprocessing,先进行LabelEncoder,再进行OnehotEncoder.
1 | from nltk import ngrams |
Word embedding
word embedding是现在非常流形的通过深度学习来解决文本数据问题的算法.Word embedding将原本稀疏的向量(onehot vector)嵌入到一个密集空间,使稀疏向量转变为密集向量.并且用这种方法使得邻的单词有相似的表达.
介绍以下所使用用的torchtext
API:
- torchtext.data
- torchtext.data.Example:用来表示一个样本,数据+标签
- torchtext.vocab.Vocab:词汇表相关
- torchtext.data.Datasets:数据集类, getitem 返回Example,制作数据集的类
- torctext.data.Field:用来定义字段的处理方法(文本字段,标签字段),这里用来定义创建Example时的预处理,batch时的一些处理操作
- torchtext.data.Iterator: 迭代器,来生成batch
torchtext.datasets:包含了常见的数据集
1
2from torchtext.data import Field,Example,TabularDataset
from torchtext.data import BucketIterator
Field:用来定义字段以及文本预处理方法
Example:用来表示一个样本,通常为 “数据+标签”
TabularDataset:用来从文件中读取数据,生成Dataset,Dataset是Example实例的集合
BucketIterator:迭代器,用来生成batch,类似的有Iterator,BucketIterator功能强大支持排序,动态padding等
TorchText的数据预处理流程:
定义样本的处理操作:torchtext.data.Field
加载corpus(都是string):torchtext.data.datasets
- 在Datasets中,torchtext将corpus处理成一个个的torchtext.data.Example实例
- 创建torchtext.data.Example的时候,会调用field.preprocess方法
- 创建词汇表,用来将string token转化成index: field.build_vocab()
- 词汇表负责:string token转换为index,index转换为 string token,string token转换为 word vector
- 将处理后的数据进行batch操作:torchtext.data.Iterator
- 将Datasets中的数据batch化
- 其中会包含一些pad操作,保证一个batch中的example长度一致
- 在这里将string token转换为index
Field对象包含的参数:
sequential:是否把数据表示成序列,如果False,不能使用分词.默认值:True
use_vocab:是否使用词典对象.如果是False数据的类型必须已经是数值类型。默认:True
init_token:每条数据的其实字符,默认值:None
eos_token:每条数据的结尾字符,默认值:None
fix_length:修改每条数据的长度为该值,不够的用pad_token补全,默认值:None
tensor_type:把数据转换成tensor类型,默认值torch.LongTensor
preprocessing:在分词之后和数值化之前使用的管道,默认值:None
postprocessing:数值化之后和转换成tensor之前使用的管道,默认值:None
lower:是否把数据转化成小写,默认值:False
tokenize:分此函数:默认值:str.split
pad_token:用于补全的字符.默认值:”
unk_token:不存在词典里的字符.默认值:”
pad_first:是否补全第一个字符.默认值:False
torchtext.TabularDataset:继承自pytorch的Dataset,提供了一个可以下载压缩数据并解压的方法(支持zip,gz,tgz)
splits方法可以同时读取训练集,验证集,测试集
TabularDataset可以很方便读取CSV,TSV或JSON格式的文件1
2
3
4train, val, test = data.TabularDataset.splits(
path='./data/', train='train.tsv',
validation='val.tsv', test='test.tsv', format='tsv',
fields=[('Text', TEXT), ('Label', LABEL)])
加载数据后可以建立词典,建立词典的时候可以使用预训练的wordvector1
TEXT.build_vocab(train,vectors="glove.6B.100d")
Iterator:是torchtext到模型的输入,它提供了我们对模型的一般处理方式,比如打乱,排序等等.可以动态修改batch大小,这里也有splits方法,可以同时输出训练集,验证集,测试集
参数如下
dataset:加载的数据集
batch_size:Batch大小
sort_key:排序的key
train:是否是一个训练集
repeat:是否在不同epoch中重复迭代
shuffle:是否打乱数据
sort:是否对数据进行排序
sort_within_batch:batch内部是否排序
device:指定设备
1 | train_iter, val_iter, test_iter = data.Iterator.splits( |
torchtext有预训练的word embedding向量
也可以根据自己任务训练,下面是一个embedding layer1
2
3
4
5
6
7
8
9class EmbNet(nn.Module):
def __init__(self,emb_size,hidden_size1,hidden_size2=400):
super().__init__()
self.embedding=nn.Embedding(emb_size,hidden_size1)
self.fc=nn.Linear(hidden_size2,3)
def forward(self,x):
embeds=self.embedding(x).view(x.size(0),-1)
out=self.fc(embeds)
return F.log_softmax(out,dim=-1)
训练过程1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26from torch.nn import functional as F
def fit(epoch,model,data_loader,phase="training",volatile=False):
if(phase=="training"):
model.train
if(phase=="validation"):
model.eval()
volatile=False
running_loss=0.0
running_correct=0
for batch_idx,batch in enumerate(data_loader):
text,target=batch.text,batch.label
if is_cuda:
text,target=text.cuda(),target.cuda()
if phase=="training":
optimizer.zero_grad()
output=model(text)
loss=F.nll_loss(output,target)
running_loss+=F.nll_loss(output,target,size_average=False).data[0]
preds=output.data.max(dim=1,keepdim=True)[1]
running_correct+=preds.eq(target.data.view_as(preds)).cpu().sum()
if(phase=="training"):
loss.backward()
optimizer.step()
loss=running_loss/len(data_loader.dataset)
accuracy=100.*running_correct/len(data_loader.dataset)
return loss,accuracy
Recursive neural networks
RNN结构已经介绍过很多次了.下面主要是介绍代码
标准的RNN网络
定义一个RNN网络1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17from torch.autograd import Variable
class RNN(nn.Module):
def __init__(self,input_size,hidden_size,output_size):
super(RNN,self).__init__()
self.hidden_size-hidden_size
self.i2h=nn.Linear(input_size+hidden_size,hidden_size)
self.i2o=nn.Linear(input_size+hidden_size,output_size)
self.softmax=nn.LogSoftmax(dim=1)
def forward(self,x,hidden):
combined=torch.cat((x,hidden),1)
hidden=self.i2h(combined)
output=self.i2o(combined)
output=self.softmax(output)
return output
def initHidden(self):
return Variable(torch.zeros(1,self.hidden_size))
设计一个循环过程传入数据1
2
3def fit(model,text):
for i in range(len(text)):
output,hidden=model(text[i],hidden)
pytorch库中的RNN
RNN的输入数据格式:[seq_len,batch_size,input_size],可以看到RNN的输入数据与我们用CNN时的数据输入格式不太一样,CNN的输入是[batch_size,channel,W,H],batch_size是第一个维度,而RNN的传入数据batch_size在第二位.seq_len是一个句子的最大长度.torch.nn.RNN()存在参数batch_first,可以修改该网络适合为batch_first
可以看到库中的RNN是自动提取seq_len,即时间步.
RNN参数1
2
3
4
5
6
7
8
9RNN
- input_dim 表示输入数据的特征维度
- hidden_dim 表示输出的特征维度,如果没有特殊变化,相当于out
- num_layers 表示网络的层数
- nonlinearity 表示选用的非线性激活函数,默认为"tanh"
- bias 表示是否使用偏置,默认使用
- batch_first 表示输入数据的形式,默认为False
- dropout 表示是否在输出层应用dropout
- bidirectional 表示是否使用双向的rnn,默认是False
例:1
2
3
4rnn_seq=nn.RNN(5,10,2)#输入特征维度为5,输出特征维度为10,层数为2
x=torch.randn(6,3,5)#句子长度为6,batch为3,5为输入特征维度
out,ht=rnn_seq(x)#h0可以不指定,默认为全零的隐藏状态
#out,ht=rnn_seq(x,h0)
这里out的size为(6,3,10),ht的size为(2,3,10)其中out的size是[seq_len,batch_size,output_dim],ht的size是[num_layers*num_direction,batch_size,hidden_size]
在每个时间点内out和ht是一样,但对于输出我们需要保存下每个时间点,因此out的第0维的大小就是seq_len,而ht不用保存每个时间点的数据,每次覆盖就行.因此第0维只和layer_num和num_direction有关.
注意:out[-1]和ht[-1]相等
这个问题有必要参考博客:
https://blog.csdn.net/VioletHan7/article/details/81292275
LSTM
Pytorch库中的LSTM
1 | #输入维度30,隐层100维,2层 |
out的size是(seq_len,batch_size,output_dim)
(h,z)的size都是(num_layersnum_direction,batch_size,output_dim)
*注:out[-1,:,:]与h[-1,:,:]
例子
- 准备数据
1 |
|
- 创造batch
这里的数据大小会是[序列长度,batch大小],这里是[200,32]
1 | train_iter,test_iter=data.BucketIterator.splits((train,test),batch_size=32) |
创造网络
embedding层的大小是词汇表大小×hidden_size(因为输入的一般是onehot)
假设hidden_size为100,经过embedding层后输出的数据大小为[200,32,100].100是embedding维度.
LSTM接受embeding层的输出伴随着2个隐变量.隐变量应该是和embedding输出相同类型的,它们的大小为[num_layers,batch_size,hidden_size].LSTM处理序列数据产生的输出形状为[Sequence_length,batch_size,hidden_size].最后一个序列的输出的形状为[batch_size,hidden_dim],将其传递到线性层中,将其映射到类别输出上.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class IMDBnn(nn.Module):
def __init__(self,n_vocab,hidden_size,n_cat,bs,nl=2):
super().__init__()
self.hidden_size=hidden_size
self.bs=bs
self.nl=nl
self.e=nn.Embedding(n_vocab,hidden_size)
self.rnn=nn.LSTM(hidden_size,hidden_size,nl)
self.fc2=nn.Linear(hidden_size,n_cat)
self.softmax=nn.LogSoftmax(dim=-1)
def forward(self,x):
bs=x.size()[1]
if(bs!=self.bs):
self.bs=bs
e_out=self.e(x)
h0=c0=Variable(e_out.data.new(*(self.nl,self.bs,self.hidden_size)).zero_())
rnn_o,_=self.rnn(e_out,(h_0,c_0))
rnn_o=rnn_o[-1]
fc=F.dropout(self.fc2(rnn_o),p=0.8)
return self.softmax(fc)训练模型
1 | model=IMDBRnn(n_vocab,n_hidden,3,bs=32) |
1 | train_losses,train_accuracy=[],[] |
GRU
1 | gru_seq=nn.GRU(10,20,2) #输入维度为10,输出维度为20,层数为2 |
在序列数据上的卷积网络
由于文本数据与图像数据相比,缺少一个channel维度.因此,这里的卷积采用的是Conv1d.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class IMDBCnn(nn.Module):
def __init__(self,n_vocab,hidden_size,n_cat,bs=1,kernel_size=3,max_len=200):
super().__init__()
self.hidden_size=hidden_size
self.bs=bs
self.e=nn.Embedding(n_vocab,hidden_size)
self.cnn=nn.Conv1d(max_len,hidden_size,kernel_size)
self.avg=nn.AdaptiveAvgPool1d(10)
self.fc=nn.Linear(1000,n_cat)
self.softmax=nn.LogSoftmax(dim=-1)
def forward(self,x):
bs=x.size()[0]
if(bs!=self.bs):
self.bs=bs
e_out=self.e(x)
cnn_o=self.cnn(e_out)
cnn_avg=self.avg(cnn_o)
cnn_avg=cnn_avg.view(self.bs,-1)
fc=F.dropout(self.fc(cnn_avg),p=0.5)
return self.softmax(fc)
1 | train_losses,train_accuracy=[],[] |
DeepNLP实践
Skip-gram-Naive-Softmax
1 | import torch |
Model
1 | class Skipgram(nn.Module): |
train
1 | EMBEDDING_SIZE=30 |
test1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19def word_similarity(target, vocab):
if USE_CUDA:
target_V = model.prediction(prepare_word(target, word2index))
else:
target_V = model.prediction(prepare_word(target, word2index))
similarities = []
for i in range(len(vocab)):
if vocab[i] == target: continue
if USE_CUDA:
vector = model.prediction(prepare_word(list(vocab)[i], word2index))
else:
vector = model.prediction(prepare_word(list(vocab)[i], word2index))
cosine_sim = F.cosine_similarity(target_V, vector).data.tolist()[0]
similarities.append([vocab[i], cosine_sim])
return sorted(similarities, key=lambda x: x[1], reverse=True)[:10] # sort by similarity
test = random.choice(list(vocab))
word_similarity(test, vocab)
Skip-gram with negative sampling
1 | corpus=list(nltk.corpus.gutenberg.sents('melville-moby_dick.txt'))[:500]; |
1 | Z=0.001 |
Negative Sampling1
2
3
4
5
6
7
8
9
10
11
12
13def negative_sampling(targets,unigram_table,k):
batch_size=targets.size(0)
neg_samples=[]
for i in range(batch_size):
nsample=[]
target_index=targets[i][0]
while(len(nsample)<k):
neg=random.choice(unigram_table)
if word2index[neg]==target_index:
continue
nsample.append(neg)
neg_samples.append(prepare_squence(nsample,word2index).view(1,-1))
return torch.cat(neg_samples)
Model
1 | class SkipgramNegSampling(nn.Module): |
Train1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28EMBEDDING_SIZE = 30
BATCH_SIZE = 256
EPOCH = 100
NEG = 10 # Num of Negative Sampling
losses = []
model = SkipgramNegSampling(len(word2index), EMBEDDING_SIZE)
if USE_CUDA:
model = model.cuda()
optimizer = optim.Adam(model.parameters(), lr=0.001)
for epoch in range(EPOCH):
for i,batch in enumerate(getBatch(BATCH_SIZE, train_data)):
inputs, targets = zip(*batch)
inputs = torch.cat(inputs) # B x 1
targets = torch.cat(targets) # B x 1
negs = negative_sampling(targets, unigram_table, NEG)
model.zero_grad()
loss = model(inputs, targets, negs)
loss.backward()
optimizer.step()
losses.append(loss.data.tolist()[0])
if epoch % 10 == 0:
print("Epoch : %d, mean_loss : %.02f" % (epoch, np.mean(losses)))
losses = []
Recurrent Neural Networks and Language Models
1 | import torch |
1 | class LanguageModel(nn.Module): |
Seq2Seq with attention
seq2seq网络的编码器是一个RNN,它为输入句子中的每个单词输出一些值。 对于每个输入单词,编码器输出一个向量和一个隐藏状态,这个隐藏状态和下一个单词构成下一步的输入。
1 | class Encoder(nn.Module): |
解码器是另一个RNN,它接收编码器输出向量并输出一个字序列来创建翻译。
在最简单的seq2seq解码器中,我们只使用编码器的最后一个输出。 这个最后的输出有时被称为上下文向量,因为它从整个序列编码上下文。 该上下文向量被用作解码器的初始隐藏状态。如果仅在编码器和解码器之间传递上下文向量,则该单个向量承担编码整个句子的负担。注意力(Attention Decoder)允许解码器网络针对解码器自身输出的每一步“聚焦”编码器输出的不同部分。首先我们计算一组注意力权重。 这些将被乘以编码器输出矢量以创建加权组合。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86class Decoder(nn.Module):
def __init__(self,input_size,embedding_size,hidden_size,n_layers=1,dropout_p=0.1):
super(Decoder,self).__init__()
self.hidden_size=hidden_size
self.n_layers=n_layers
self.embedding=nn.Embedding(input_size,embedding_size)
self.dropout=nn.Dropout(dropout_p)
self.gru=nn.GRU(embedding_size+hidden_size,hidden_size,n_layers,batch_first=True)
self.linear=nn.Linear(hidden_size*2,input_size)
self.attn=nn.Linear(hidden_size,self.hidden_size)
def init_hidden(self,inputs):
hidden=Variable(torch.zeros(self.n_layers,inputs.size(0),self.hidden_size))
return hidden.cuda()
def init_weight(self):
self.embedding.weight=nn.init.xavier_uniform(self.embedding.weight)
self.gru.weight_hh_10=nn.init.xavier_uniform(self.gru.weight_hh_10)
self.gru.weight_ih_10=nn.init.xavier_uniform(self.gru.weight_ih_10)
self.linear.weight=nn.init.xavier_uniform(self.linear.weight)
self.attn.weight=nn.init.xavier_uniform(self.attn.weight)
def Attention(self,hidden,encoder_outputs,encoder_maskings):##attention的计算过程,hidden是decoder的隐层
'''
hidden:1,B,D
encoder_output:B,T,D
encoder_mask:B,T
'''
hidden=hidden[0].unsqueeze(2)#B,1,D
batch_size=encoder_outputs.size(1)
max_len=encoder_outputs.size(1)
energies=self.attn(encoder_outputs.contiguous().view(batch_size*max_len,-1))
energies=energies.view(batch_size,max_len,-1)
attn_energies=energies.bmm(hidden).squeeze(2)##Encoder的隐层和decoder的隐层相乘
alpha=F.softmax(attn_energies,1)##分数alpha
alpha=alpha.unsqueeze(1)
context=alpha.bmm(encoder_outputs)
return context,alpha
def forward(self,inputs,context,max_length,encoder_outputs,encoder_maskings=None,is_training=False):##inputs是开始符,context是Encoder的输出hidden_c,encoder_outputs是encoderd的序列输出
embedded=self.embedding(inputs)
hidden=self.init_hidden(inputs)
if is_training:
embedded=self.dropout(embedded)
decode=[]
for i in range(max_length):
_,hidden=self.gru(torch.cat((embedded,context),2),hidden)
concated=torch.cat(hidden,context.transpose(0,1),2)
score=self.liear(concated.squeeze(0))
softmaxed=F.log_softmax(score,1)
decode.append(softmaxed)
decoded=softmaxed.max(1)[1]##预测的分数或说是预测的结果
##重复embedded的生成过程
embedded=self.embedding(decoded).unsqueeze(1)
if is_training:
embedded=self.dropout(embedded)
context,alpha=self.Attention(hidden,encoder_outputs,encoder_maskings)
scores=torch.cat(decode,1)
return scores.view(inputs.size(0)*maxl_length,-1)
def decode(self,context,encoder_outputs):
start_decode=Variable(torch.LongTensor([[target2index['<s>']]*1])).transpose(0,1)
embedded=self.embedding(start_decode)
hidden=self.init_hidden(start_decode)
decodes=[]
attentions=[]
decoded=embedded
while decoded.data.tolist()[0]!=target2index['</s>']:
_,hidden=self.gru(torch.cat((embedded,context),2),hidden)
concated=torch.cat((hidden,context.transpose(0,1)),2)
score=self.linear(concated.squeeze(0))
softmaxed=F.log_softmax(score,1)
decodes.append(softmaxed)
decoded=softmaxed.max(1)[1]
embedded=self.embedding(decoded).unsqueeze(1)
context,alpha=self.Attention(hidden,encoder_outputs,None)
attentions.append(alpha.squeeze(1))
return torch.cat(decodes).max(1)[1],torch.cat(attentions)