深度神经网络在NLP的应用!
Posted 专知
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深度神经网络在NLP的应用!相关的知识,希望对你有一定的参考价值。
神经网络结构对自然语言处理领域的影响最深刻;
深度学习可以对自然语言处理的各个层面制定学习任务;
密集词汇表示的重要性和学习表示的方法。
1. 数据及背景
2. 文本表示
传统离散式文本表示主要是one-hot编码以及tf-idf等编码方式,但往往存在以下问题:
-
无法衡量词向量之间的关系。 -
词表的维度随着语料库的增长而膨胀。 -
n-gram词序列随语料库增长呈指数型膨胀,更加快。 -
离散数据来表示文本会带来数据稀疏问题,导致丢失了信息,与我们生活中理解的信息是不一样的。
2.1 词嵌入
将所有词语投影到K维的向量空间,每个词语都可以用一个K维向量表示。好处是Distributed representation — word embedding:这种非稀疏表示的向量很容易求它们之间的距离(欧式、余弦等),从而判断词与词语义上的相似性,也就解决了one-hot方法表示两个词之间的相互独立的问题。
下面介绍的Word2Vec属于NNLM模型,即神经网络语言模型(Neural Network Language model),它是通过训练得到词向量矩阵,这就是我们要得到的文本表示向量矩阵。NNLM说的是定义一个前向窗口大小,把这个窗口中最后一个词当做y,把之前的词当做输入x。通俗来说就是预测这个窗口中最后一个词出现概率的模型。
2.2 Word2vec
Word2Vec模型是一种简单化的神经网络,用于对文本的分布式表示。它包含了两个训练模型(Skip-gram,CBOW)以及两种加速的方法(Hierarchical Softmax,Negative Sampling)。
2.2.1 CBOW 模型
CBOW模型是在已知当前词上下文context的前提下预测当前词w(t),类似阅读理解中的完形填空。
2.2.2 Skip-Gram 模型
相反,Skip-Gram模型是在已知当前词w(t)的前提下,预测上下文context。Skip-gram 模型的目标是最大化对数条件概率:
里面的条件概率即softmax函数:
u是权重(也是这个词的词向量),v就是所要迭代的向量,将该式子代入上式并取log可以得到:
对上述目标函数求最大化,可以让相似的词具有相似的向量值。
Softmax 分母是词汇表大小的求和,其复杂度比较高,需要提出快速又有效的算法,于是就有了Negtive sampling 以及 Hierarchical Softmax。
2.2.3 Negative Sampling
正例:中心词是 w,它周围上下文共有2c个词,记为 。由于这个中心词 ,的确和 相关存在,因此它是一组正例。
负例:通过负采样,我们得到 (负采样的个数)个和 w 不同的中心词wi,i=1,2,..neg,这样 和 就组成了 组并不真实存在的负例。
数据:
-
正:x:context(w),label:
-
负:x:context(w),label: 。
利用这一个正例和 个负例,我们进行二元逻辑回归(可以理解成一个二分类问题),得到负采样对应每个词 对应的模型参数以及每个词的词向量。
更新的参数 = 正的对数概率 - 负的对数概率的期望(服从词频分布)
上述问题可以看成二分类问题,于是我们得出有目标函数:
2.2.4 Hierarchical Softmax
输入层到投影层是把输入层的所有向量进行加和给投影层,比如,输入的是三个4维词向量:(1,2,3,4),(9,6,11,8),(5,10,7,12),那么我们word2vec映射后的归一化词向量就是(5,6,7,8),对CBOW
模型来说,就是把上下文词向量加和,然而,对于Skip-Gram
模型来说就是简单的传值。
最后的输出是构建一颗哈夫曼树,如何去构造简单的哈夫曼树。在这里不在累述;在这里,哈夫曼树的所有叶子节点是词表中的所有词,权值是每个词在词表中出现的次数,也就是词频。
一般得到哈夫曼树后我们会对叶子节点进行哈夫曼编码,由于权重高的叶子节点越靠近根节点,而权重低的叶子节点会远离根节点,这样我们的高权重节点编码值较短,而低权重值编码值较长。这保证的树的带权路径最短,也符合我们的信息论,即我们希望越常用的词(词频越高的词)拥有更短的编码,一般的编码规则是左0右1,但是这都是人为规定的,word2vec中正好采用了相反的编码规则,同时约定左子树的权重不小于右子树的权重。在word2vec中,采用了二元逻辑回归的方法,即规定沿着左子树走,那么就是负类(哈夫曼树编码1),沿着右子树走,那么就是正类(哈夫曼树编码0)。
2.2.5 Word2Vec存在的问题
-
对每个local context window单独训练,没有利用包含在global中的统计信息。
-
改进:Glove,用了全局的信息(共线矩阵) -
对多义词无法很好的表示和处理,因为使用了唯一的词向量。
-
改进:Gaussian Embedding,将词向量加入一个高斯噪音,方差由词频决定。
2.2.6 FastText 和 Word2vec 中的层次 softmax 区别
我们之前学习的 FastText 中输出层也用到了层次 softmax,但CBOW的叶子节点是词和词频,而 FastText 的叶子节点是类标和类标的频数。两者本质不同,体现在 h-softmax 的使用:
Word2Vec | FastText | |
---|---|---|
输入 | one-hot形式的单词的向量 | embedding过的单词的词向量和n-gram向量 |
输出 | 对应的是每一个term,计算某term概率最大 | 对应的是分类的标签。 |
word2vec 的目的是得到词向量,该词向量最终是在输入层得到的,输出层对应的 h-softmax 也会生成一系列的向量,但是最终都被抛弃,不会使用。
FastText 则充分利用了 h-softmax 的分类功能,遍历分类树的所有叶节点,找到概率最大的 label
2.2.7 Word2vec实践代码
我们知道在 Tf-Idf 的训练时,可以将训练集和测试集一起进行训练,那么 Word2vec 也是一样的,以下是实现方式:
# 构造word2vec的输入,corpus
all_df=train.append(test)
all_df["corpus"]=all_df.text.apply(lambda x:x.split(" "))
corpus=all_df.corpus.values
# 开始训练
def hashfxn(x):
return int(x)
logging.basicConfig(
format='%(asctime)s : %(levelname)s : %(message)s',
level=logging.INFO
)
model = Word2Vec(
corpus,workers=multiprocessing.cpu_count(),
size=300,sg=1,hs=0,negative=5,
hashfxn=hashfxn,min_count=3,iter=5
)
# 保存
model.wv.save_word2vec_format("../data/word2vec.bin",binary=True)
# 读取
w2v_path="../data/word2vec.bin"
word_vectors = gensim.models.KeyedVectors.load_word2vec_format(w2v_path, binary=True)
3. 文本分类
3.1 TextCNN
TextCNN利用CNN(卷积神经网络)进行文本特征抽取,不同大小的卷积核分别抽取n-gram特征,卷积计算出的特征图经过MaxPooling保留最大的特征值,然后将拼接成一个向量作为文本的表示。
这里我们基于TextCNN原始论文的设定,分别采用了100个大小为2,3,4的卷积核,最后得到的文本向量大小为100*3=300维。
3.1.1 维度分析
input 层(m句sentence,句子长度为n) :m=batch_size → 进入 embedding 层 , 变为 m×n×300 。Conv2d 只能做四维卷积,要把第1维unsqueeze,, 变为 m×1×n×300,这里也可以尝试Conv1d → 进入 conv 层 被卷积,一个大小为(2,300) 的二维卷积核 之后变为 m×1×(n-1)×1(无padding),若是大小为3变为 m×1×(n-2)×1(无padding) → 进入 pooling 层 ,每一个向量 变为 m×1×1,取了300个卷积核(out_channels),所以维度为 m×1×300 → 进入 classification 层,输出各标签的概率 m×num_class。
细节:在batch处理的时候,每个batch一起计算,为了保证kernel卷积出来的长度相等,所以每个batch padding成一个长度几乎是必需的。
3.1.2 ModelEmbedding 类细节
首先,我们构造了 ModelEmbedding
类来解决预训练词向量的预处理。
我们在词典里加入了 PADDING
和 UNKNOWN
两个字符,对应之后 padding_value=0
,和 getitem
里面的UNKNOWN
,当然,还要构造词典和词典长度的获取。
def __init__(self, w2v_path):
# 加载词向量矩阵
self.word2vector = gensim.models.KeyedVectors.load_word2vec_format(w2v_path, binary=True)
# 输入的总词数
self.vocabs = self.word2vector.vocab.keys()
special_token_list = ["<PADDING>", "<UNKNOWN>"]
self.input_word_list = special_token_list + self.word2vector.index2word
self.embedding = np.concatenate((np.random.rand(2, 300), self.word2vector.vectors))
self.dict_length = len(self.input_word_list)
self.get_word2id()
def get_word2id(self):
# word2id词典
self.word2id = dict(zip(self.input_word_list, list(range(len(self.input_word_list)))))
3.1.3 Dataset 类细节
其次,我们构造了一个 Dataset
类 和一个 GetLoader
类来获取输入网络的batch。这里的句子长度实在时不好统一,所以我们采用 max_length
进行截断,但是这里只是我主观的想法,并没有实际尝试截断多少为最好的。
def __getitem__(self, item):
temp = []
for word in self.corpus[item]:
if word in self.model_embedding.word2id:
temp.append(self.model_embedding.word2id[word])
else:
temp.append(self.model_embedding.word2id['<UNKNOWN>'])
if len(temp) > self.max_length:
temp = temp[:self.max_length]
if self.with_label:
return torch.tensor(temp), self.corpus_label[item]
else:
return torch.tensor(temp)
3.1.4 GetLoader 类& collate_fn 细节:
这里需要注意collect_fn
的细节,测试集不能被打乱,所以这里针对不同的数据集构造了不同的 collect_fn
。
def get_iter(self):
def collate_fn_train(batch_data):
batch_data.sort(key=lambda xi: len(xi[0]), reverse=True)
data_length = [len(xi[0]) for xi in batch_data]
sent_seq = [xi[0] for xi in batch_data]
label = [xi[1] for xi in batch_data]
padded_sent_seq = pad_sequence(sent_seq, batch_first=True, padding_value=0)
return padded_sent_seq, data_length, torch.tensor(label, dtype=torch.long)
def collate_fn_test(batch_data):
data_length = [len(xi) for xi in batch_data]
sent_seq = [xi for xi in batch_data]
padded_sent_seq = pad_sequence(sent_seq, batch_first=True, padding_value=0)
return padded_sent_seq, data_length
self.train_loader = DataLoader(self.train_dataset, batch_size=64, collate_fn=collate_fn_train)
self.valid_loader = DataLoader(self.valid_dataset, batch_size=256, collate_fn=collate_fn_train)
self.test_loader = DataLoader(self.test_dataset, batch_size=256, collate_fn=collate_fn_test)
最后我们构造了 TextCNN 类进行模型的搭建,有需要的可以去Github自行提取,这里提一下 预训练词向量载入的方法。
3.1.5 TextCNN 类细节
预载入的词向量pretrained_word_vectors
,就是我们刚才构造的 ModelEmbedding.embedding,和直接用 gensim
的载入的唯一区别就是我们多了两个单词 PADDING
和 UNKNOWN
,他们被随机初始化了。
def init_weights(self, pretrained_word_vectors, is_static=False):
self.embedding.weight = nn.Parameter(torch.from_numpy(pretrained_word_vectors).float())
if is_static:
self.embedding.weight.requires_grad = False
else:
self.embedding.weight.requires_grad = True
最后,只要套用训练模板进行训练即可,这里展示一下跑程序的参考流程
if __name__ == "__main__":
data_root = {
"train_path": './data/train_torch.csv',
"test_path": "./data/test_a.csv",
"sub_path": "./data/test_a_sample_submit.csv",
"w2v_path": "./data/word2vec.bin"
}
config = GetInit(data_root)
model_embedding = ModelEmbedding(data_root["w2v_path"])
train_dataset = MyDataset(model_embedding,
corpus=config.x_train,
corpus_label=config.y_train,
with_label=True)
test_dataset = MyDataset(model_embedding,
corpus=config.x_test,
with_label=False)
loader = GetLoader(train_dataset, test_dataset)
# 建立model
model = TextCNN(
vocab_size=model_embedding.dict_length,
embedding_dim=300,
output_size=14
)
model.init_weights(model_embedding.embedding, is_static=False)
model = model.cuda()
criterion = nn.NLLLoss()
opt = torch.optim.Adam(model.parameters(), lr=1e-4)
# 开始训练
mytrain = TrainFunc(model, criterion, opt, loader.train_loader, loader.valid_loader, loader.test_loader)
best_model = mytrain.train(20)
最后使用了10%的训练集作为验证集,在线上无调参的TextCNN得分为0.937,冲入了前30,可见Word2vec+TextCNN 还是非常不错的一个模型,当然我这里没有取定随机数,shake 还是挺大的。
3.2 TextRNN
有了上面的框架之后我们只需要改掉 我们的分类器TextCNN变为 TextRNN 即可。让我们来做一下维度分析:
3.2.1 维度分析
这里我们首先采用已经 padding 和截断过的长度,所以每个batch都是一样的。(因为不用写代码……)
input 层(m句sentence,句子长度为n) :m=batch_size → 进入 embedding 层 , 变为 m×n×300
。→ 进入 LSTM 变为 m×n×hidden_size
, 这里的hidden_size
是LSTM
输出的维度 → 我们只取最后一个长度作为输出,维度变为 m×hidden_size
→ 进入 classification 层(全连接层),输出各标签的概率 m× num_class
。
3.2.2 LSTM复习
写到这,我不禁想默写一下 LSTM 的公式。
3.2.3 TextRNN代码实践
我们的完整代码只需要更改分类器TextRNN即可,实践代码已经上传至Github,可能需要改大一点的lr,LSTM 的前几个epoch 太不好 train,这种是一个比较简单的网络层。
-
注意到LSTM的输出,其实我们只要第一项output,但是我觉得拿 hn 也可以,总之都差不多,因为output是所有的h。 -
在全连接层之前,还要取最后一个的输出, out[:, -1, :]
。 -
注意全连接的输入维度和双向单向有关系,双向要翻倍。 -
教材里有个维度交换的过程,是用来调整 batch_first 的顺序把,但是我这里验了一下觉得没有必要,所以就没换。 -
看到有些同学初始化了一下 h0 和 c0,我觉得不错,但是我这里就不用了,我喜欢代码简洁一点。
class TextRNN(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_size=100, output_size=14, dropout=0.5):
super(TextRNN, self).__init__()
self.embedding = nn.Embedding(vocab_size, embedding_dim)
self.word_lstm = nn.LSTM(input_size=embedding_dim,
hidden_size=hidden_size,
num_layers=1,
batch_first=True,
bidirectional=True)
self.dropout = nn.Dropout(dropout)
self.fc = nn.Linear(hidden_size * 2, output_size)
def init_weights(self, pre_trained_word_vectors, is_static=False):
self.embedding.weight = nn.Parameter(torch.from_numpy(pre_trained_word_vectors).float())
if is_static:
self.embedding.weight.requires_grad = False
else:
self.embedding.weight.requires_grad = True
def forward(self, inputs):
out = self.embedding(inputs)
out, _ = self.word_lstm(out)
out = self.dropout(out)
out = self.fc(out[:, -1, :])
return F.log_softmax(out, 1)
3.2.4 TextRNN实践结果
不理想,LSTM 迭代速度较慢是老问题了,学习率比TextCNN要大一点,但是在验证集上只能达到0.92,所以没有提交,猜想是padding和LSTM不搭的关系,顺便学习一下torch 如何输入变长序列,结果一开始彻底没有train起来,后来通过小数据调参,找到了问题,用RMSprop替换 Adam 最后训练成功,真是玄学啊。
3.2.5 变长序列
细节有三点,一个是模型的输入要变,第二个是模型的处理要变,第三个是长度截断。
-
模型输入:输入从单纯的矩阵变成了一个打包了数据和长度的东西,我们要把长度和数据都传给模型,这样他才能从padding好的数据里,找到自己需要的结尾。
from torch.nn.utils.rnn import pack_padded_sequence
# 老版本是torch.nn.utils.rnn.rnn_utils.pack_padded_sequence
for batch_idx, (data, length, label) in enumerate(iter(self.train_iter)):
batch_x_pack = pack_padded_sequence(data, length, batch_first=True)
batch_x_pack = batch_x_pack.cuda()
label = label.cuda()
output = self.model(batch_x_pack)
-
模型处理:主要是forward里面的处理,先把打包的数据拆了,让数据 data 过 Embedding 层和 Lstm层,在取输出的时候用 length 的列表去取。 -
需要注意这里新建立了一个tensor,所以GPU 要过 cuda(),其他都是相似的。 -
或者像我一样使用Pytorch 花式索引,让代码更简洁。
# def forward(self, inputs):
# out = self.embedding(inputs)
# out, _ = self.word_lstm(out)
# out = self.dropout(out)
# out = self.fc(out[:, -1, :])
# return F.log_softmax(out, 1)
def forward(self, x):
_pad, _len = pad_packed_sequence(x, batch_first=True)
x_embedding = self.embedding(_pad)
# x_embedding = x_embedding.view(x_embedding.shape[0], x_embedding.shape[1], -1)
output, _ = self.word_lstm(x_embedding)
_len = _len - 1 # 每个元素-1
#temp = []
#for i in range(len(_len)):
# temp.append(output[i, _len[i], :].tolist())
#temp = torch.tensor(temp).cuda()
temp = output[range(len(_len)),_len,:]
# pytorch花式索引与上面注释掉的等价
out = self.fc(temp)
return F.log_softmax(out, 1)
-
长度截断:我个人认为可以使用更长的长度,但是模型并没有更好,甚至没有 train 起来,仔细想想长度太长的确训练不起来,这里不能使用分词,以字为单位,LSTM效果的确不理想。
3.2.6 TextCNN与TextRNN 总结
在匿名字符集上,TextCNN 感觉思路上比RNN 更高一筹,训练速度更是比 RNN快,但是CNN一定要padding,就让人觉得很变扭。
3.2.7 得分总结
TextCNN:线下 0.938 ,线上0.937 TextRNN 等长:线下 0.932,线上未提交。TextRNN 变长:线下 0.928,线上未提交。
3.3 HAN
3.3.1 结构
HAN的结构与 TextCNN或者TextRNN 是两个不一样层面的东西,最重要的体现是他们的输入不一样。HAN的文本层次是这样的:词→句→文。而TextCNN或者TextRNN的文本层次为:词→文。所以迥异的结构也导致了预处理代码要重新整理一下,要进行分句,但是这里是匿名数据集,都是匿名字符,所以要做一点EDA实验,才能很好的分句。
3.3.2 维度分析
HAN的结构为5层:我们结合维度分析来拆解。关于输入,我们的句子数和句子长度,都是被截断过和padding过的,所以在每个batch都是等长的。
input 层(b个文档,m句sentence,句子长度为n) :b=batch_size → 进入 embedding 层 , 变为 b×m×n×300
。→ 进入 GRU1 word层 变为 b×m×n×hidden_size1
, 这里的hidden_size1
是GRU1
输出的维度,我们这里取全部步长,而不再是最后一个长度。→ 进入 Attention1 word层 变为 b×m×hidden_size1
,将n个单词综合,代表了句子的向量表示。→ 进入 GRU2 sentence层 变为 b×m×hidden_size2
, 这里的hidden_size2
是GRU2
输出的维度。→ 进入 Attention2 sentence层 变为 b×hidden_size2
,将m个句子综合,代表文章的向量表示。→ 进入 classification 层(全连接层),输出各标签的概率 b× num_class
。
所以,可以很容易知道,他的结构层次更多一点,而我们只要学会了Attention 层,就可进行这个实现,当然,关于变长序列还是要在花点功夫,建议了解一下 Pytorch 之 PackedSequence 。
3.3.3 Attention
Attention的本质是要找到输入的feature的权重分布。
Attention的参数
如果我们输入一个长为 n 的 feature,那么 Attention 就要学习一个长为 n 的分布weight,这个分布代表着feature 中每个元素的权重,显然这个weight 是要被归一化的(sum=1),而且他就是我们要学习的参数,它是由通过计算相似度实现的。
Attention的返回
Attention 并不返回他学到参数,定义输入的feature 为 H ,学习的参数为A,代表权重分布。
那么他的返回即为s,代表这次学习到的参数的分数,也是在HAN中的句子或者文章的向量表示。
HAN中的Attention细节
Attention 有很多种计算方式,有soft ,hard,genral,concat,self ……等等等,我们这里学习的是HAN的论文中的公式。
以 word-att 层为例:句子长度为n 我们的输入为 GRU1层的output ,维度可以看做为 n×hidden_size1
,定义为 H,外面的 b×m
其实只要看成一个for 循环就可以了,或者看成长度为 b×m×n
也没问题 。
第一步:线性变换:H 变为了 U,维度可能会变换,但是只是线性变换。
第二步:学习参数:随机初始化辅助参数 ,他的维度和 一样的,通过 和 来学习权重参数A。这里已经利用了 softmax 归一化A。
最后得到的A长度也和我们的输入吻合。
第三步:计算score,即为我们要返回的东西。
总之,中间有一个向量点积计算相似度的过程,所以一定要确保点积运算的那个东西是向量,不然会报错(后注:事实上是用矩阵直接算的 ),这使得实现起来会非常的困难,我看到的比较多的实现都是三层的层级关系,工程量很大。
其实,我们已经知道最后A的维度了,反正 都是随机初始化的,那么实现的时候也可以用一个线性层直接转换到需要的维度(1)再进行softmax(很多代码都是这样实现的),不过这样好像就不是论文中的样子了。
学习权重的公式就变成了:
代码实践
代码已经上传至Github
如果按照论文中的实现,我们需要构造三个层级关系,难度较高,所以使用比较简单的实现方式,更新后的Attention代码就和论文中一样了,不过他是针对三维输入的,不能直接套进我自己的代码中。
class HAN_Attention_revise(nn.Module):
"""
HAN的attention更新,dot做法
sentence_level:inputsize=b*l*300,outsize=b*300
"""
def __init__(self,in_size=300,hidden_size=200):
super(HAN_Attention_revise,self).__init__()
self.layer1=nn.Linear(in_size,hidden_size)
self.us=nn.Parameter(torch.FloatTensor(hidden_size),requires_grad=True) # 200
def forward(self,X):
X1 = X.view(X.shape[1],X.shape[0],X.shape[2]) # l前置, l*b*300
U = torch.tanh(self.layer1(X1)) # 构造U l*b*200
A = (U*self.us).sum(2) # 点积,维度 U*self.us = l*b*200
A = F.softmax(A.T,1).unsqueeze(2) # softmax操作+转置, b*l*1 第1维归一化
return(X*A).sum(1)
下面是更新前的代码,可以针对4维输入和3维输入做attention,相似度计算方式为mlp。
class Attention(nn.Module):
def __init__(self,in_size,hidden_size):
super(Attention,self).__init__()
self.layer1=nn.Linear(in_size,hidden_size)
self.layer2=nn.Linear(hidden_size,1)
def forward(self,X):
out = self.layer1(X)
out = torch.tanh(out)
out = self.layer2(out)
out = F.softmax(out,dim=-2)
return out.mul(X).sum(-2)
我们保证在倒数第二维上归一化,在倒数第二维上求和,这样就不用在写一遍代码了。我们可以测试一下,符合预期。
# test1
p=torch.rand(64,30,254,300)
att=Attention(in_size=300,hidden_size=200)
A=att(p)
print(A.shape)
# output
torch.Size([64, 30, 300])
# test2
p=torch.rand(64,30,100)
att=Attention(in_size=100,hidden_size=50)
A=att(p)
print(A.shape)
# output
torch.Size([64, 100])
3.3.4 HAN代码
class HAN(nn.Module):
def __init__(self,vocab_size, embedding_dim,hidden_size=100, output_size=14, dropout=0.5):
super(HAN,self).__init__()
self.embedding = nn.Embedding(vocab_size, embedding_dim)
self.gru1 = nn.GRU(embedding_dim, hidden_size, bidirectional=True, batch_first=True)
self.att1 = Attention(hidden_size*2,128)
self.gru2 = nn.GRU(hidden_size*2, hidden_size//2, bidirectional=True, batch_first=True)
self.att2 = Attention(hidden_size,64)
self.dropout = nn.Dropout(dropout)
self.fc = nn.Linear(hidden_size, output_size)
def init_weights(self, pretrained_word_vectors,is_static=False):
self.embedding.weight = nn.Parameter(torch.from_numpy(pretrained_word_vectors).float())
if is_static:
self.embedding.weight.requires_grad = False
else:
self.embedding.weight.requires_grad = True
def forward(self,X):
X = self.embedding(X)
out = X.view(X.shape[0]*X.shape[1],X.shape[2], -1).contiguous()
out, _ = self.gru1(out)
out = out.view(X.shape[0],X.shape[1],X.shape[2], -1).contiguous()
out = self.att1(out)
out, _ = self.gru2(out)
out = self.att2(out)
out = self.dropout(out)
out = self.fc(out)
return F.log_softmax(out, 1)
细节:由于我们Embedding 是个 4维向量, GRU接受不了,所以我们在word级别的时候,把前两维先合并,然后再拆开。让我不禁感叹,我做了半天的预处理结果还是要合并。
3.3.5 数据预处理
没有做EDA,直接就以 3750 | 900 | 648 三个数字一起切句子,设定每篇文章最大句子数为40,每个句子最大单词数为40,也就是说我们的输入维度一定是batchsize×40×40
,这里的超参数明显是可以调的。至于怎么做二维padding,研究了好久,最终还是以比较丑陋的代码写了出来,就不放出来了,代码也发出来了,可以自行查看(改了Dataset类)。
3.3.6 其他细节
-
HAN 的训练速度比较慢,但是效率还是很高的,几个epoch就能到好的数值。 -
使用了梯度裁剪,不知道有没有起有效的作用。 -
还是只有 RMSProp 能 train 起来。 -
增加了一个lr_schedule,把玩一下。 -
这一次将train_batch的 shuffle=True,以前不shuffle是想做对比。 -
HAN线下成绩:9437,线上成绩:0.9426,冲入了前15,还是很强的。
专知便捷查看
后台回复“自然语言处理” 可以获取《自然语言处理专知资源索引》专知下载链接索引
以上是关于深度神经网络在NLP的应用!的主要内容,如果未能解决你的问题,请参考以下文章