前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深度神经网络在NLP的应用!

深度神经网络在NLP的应用!

作者头像
Datawhale
发布2020-08-20 15:15:42
6520
发布2020-08-20 15:15:42
举报
文章被收录于专栏:Datawhale专栏Datawhale专栏

作者:张泽,华东师范大学,Datawhale优秀学习者

深度学习正在给自然语言处理带来巨大的变革,例如机器翻译,情感分析,问答系统等落地实践。深度学习对NLP的影响主要有如下几点:

  • 神经网络结构对自然语言处理领域的影响最深刻;
  • 深度学习可以对自然语言处理的各个层面制定学习任务;
  • 密集词汇表示的重要性和学习表示的方法。

今天,我们以文本分类为实践,详解深度神经网络在自然语言处理中的应用。

1. 数据及背景

https://tianchi.aliyun.com/competition/entrance/531810/information(阿里天池-零基础入门NLP赛事)

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 模型的目标是最大化对数条件概率:

\large \arg \max _{\theta} \prod_{w \in DICT}\left[\prod_{c \in C(w)} p(c | w ; \theta)\right]

里面的条件概率即softmax函数:

\large \operatorname{softmax}\left(u_{o}^{T} v_{c}\right)=p(o | c)=\hat{y}_{o}=\Large \frac{e^{u_{o}^{T} v_{c}}}{\sum_{w=1}^{W} e^{u_{w}^{T} v_{c}}}

u是权重(也是这个词的词向量),v就是所要迭代的向量,将该式子代入上式并取log可以得到:

\arg \max _{\theta} \sum_{(w, c) \in D} \log p(c | w)=\sum_{(w, c) \in D}\left(\log e^{v_{c} \cdot v_{w}}-\log \sum_{c^{\prime}} e^{v_{c^{\prime}} \cdot v_{w}}\right)

对上述目标函数求最大化,可以让相似的词具有相似的向量值。

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:,。

利用这一个正例和 个负例,我们进行二元逻辑回归(可以理解成一个二分类问题),得到负采样对应每个词 对应的模型参数以及每个词的词向量。

J(\theta)=\frac{1}{T} \sum_{t=1}^{T} J_{t}(\theta) \\ J_{t}(\theta)=\log \sigma\left(u_{o}^{T} v_{c}\right)+\sum_{\small i = 1}^{k} \mathbb{E}_{j} P(w)\left[\log \sigma\left(-u_{j}^{T} v_{c}\right)\right]

更新的参数 = 正的对数概率 - 负的对数概率的期望(服从词频分布)

上述问题可以看成二分类问题,于是我们得出有目标函数:

\arg \max _{\theta} \sum_{(w, c) \in D} \log \sigma\left(v_{e} \cdot v_{w}\right)+\sum_{(w, c) \in D^{\prime}} \log \sigma\left(-v_{e} \cdot v_{w}\right)

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 也是一样的,以下是实现方式:

代码语言:javascript
复制
# 构造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 类来解决预训练词向量的预处理。

我们在词典里加入了 PADDINGUNKNOWN 两个字符,对应之后 padding_value=0,和 getitem 里面的UNKNOWN,当然,还要构造词典和词典长度的获取。

代码语言:javascript
复制
    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 进行截断,但是这里只是我主观的想法,并没有实际尝试截断多少为最好的。

代码语言:javascript
复制
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

代码语言:javascript
复制
    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 的载入的唯一区别就是我们多了两个单词 PADDINGUNKNOWN,他们被随机初始化了。

代码语言:javascript
复制
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

最后,只要套用训练模板进行训练即可,这里展示一下跑程序的参考流程

代码语言:javascript
复制
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_sizeLSTM输出的维度 → 我们只取最后一个长度作为输出,维度变为 m×hidden_size→ 进入 classification 层(全连接层),输出各标签的概率 m× num_class

3.2.2 LSTM复习

写到这,我不禁想默写一下 LSTM 的公式。

\begin{array}{ll} \\ i_t = \sigma(W_i [x_t, h_{t-1}]) \\ f_t = \sigma(W_f [x_t, h_{t-1}]) \\ g_t = \tanh(W_g [x_t, h_{t-1}]) \\ o_t = \sigma(W_o [x_t, h_{t-1}]) \\ c_t = f_t * c_{(t-1)} + i_t * g_t \\ h_t = o_t * \tanh(c_t) \\ \end{array}

3.2.3 TextRNN代码实践

我们的完整代码只需要更改分类器TextRNN即可,实践代码已经上传至Github,可能需要改大一点的lr,LSTM 的前几个epoch 太不好 train,这种是一个比较简单的网络层。

  • 注意到LSTM的输出,其实我们只要第一项output,但是我觉得拿 hn 也可以,总之都差不多,因为output是所有的h。
  • 在全连接层之前,还要取最后一个的输出,out[:, -1, :]
  • 注意全连接的输入维度和双向单向有关系,双向要翻倍。
  • 教材里有个维度交换的过程,是用来调整 batch_first 的顺序把,但是我这里验了一下觉得没有必要,所以就没换。
  • 看到有些同学初始化了一下 h0 和 c0,我觉得不错,但是我这里就不用了,我喜欢代码简洁一点。
代码语言:javascript
复制
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好的数据里,找到自己需要的结尾。
代码语言:javascript
复制
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 花式索引,让代码更简洁。
代码语言:javascript
复制
    # 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_size1GRU1输出的维度,我们这里取全部步长,而不再是最后一个长度。→ 进入 Attention1 word层 变为 b×m×hidden_size1,将n个单词综合,代表了句子的向量表示。→ 进入 GRU2 sentence层 变为 b×m×hidden_size2, 这里的hidden_size2GRU2输出的维度。→ 进入 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,代表权重分布。

H=[h_1,....,h_n]\\ A=[a_1,...,a_n]

那么他的返回即为s,代表这次学习到的参数的分数,也是在HAN中的句子或者文章的向量表示。

s=\sum_i a_i h_i

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,维度可能会变换,但是只是线性变换。

u_{i}=\tanh \left(W_{w} h_{i}+b_{w}\right)\\ H=[h_1,....,h_n]^T,\text {dimH}=n*h1 \\ U=[u_1,....,u_n]^T,\text {dimU}=n*p \\ \text {dim} u_i= p*1

第二步:学习参数:随机初始化辅助参数 ,他的维度和 一样的,通过和 来学习权重参数A。这里已经利用了 softmax 归一化A。

\alpha_{i}=\frac{\exp \left(u_{i}^{\top} u_{w}\right)}{\sum_{i} \exp \left(u_{i}^{\top} u_{w}\right)} \in R\\ A=[a1,…,an],\text {dimA}=n*1

最后得到的A长度也和我们的输入吻合。

第三步:计算score,即为我们要返回的东西。

s=\sum_{i} \alpha_{i} h_{i}

总之,中间有一个向量点积计算相似度的过程,所以一定要确保点积运算的那个东西是向量,不然会报错(后注:事实上是用矩阵直接算的 ),这使得实现起来会非常的困难,我看到的比较多的实现都是三层的层级关系,工程量很大。

其实,我们已经知道最后A的维度了,反正 都是随机初始化的,那么实现的时候也可以用一个线性层直接转换到需要的维度(1)再进行softmax(很多代码都是这样实现的),不过这样好像就不是论文中的样子了。

学习权重的公式就变成了:

\alpha_{i}=\frac{\exp (W_1U+b)}{\sum_{i} \exp (W_1U+b)} \in R\\ A=[a1,…,an]

代码实践

代码已经上传至Github

如果按照论文中的实现,我们需要构造三个层级关系,难度较高,所以使用比较简单的实现方式,更新后的Attention代码就和论文中一样了,不过他是针对三维输入的,不能直接套进我自己的代码中。

代码语言:javascript
复制
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。

代码语言:javascript
复制
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)

我们保证在倒数第二维上归一化,在倒数第二维上求和,这样就不用在写一遍代码了。我们可以测试一下,符合预期。

代码语言:javascript
复制
# 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代码

代码语言:javascript
复制
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,还是很强的。

本文PDF电子版教程 后台回复 文本分类 获取

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-08-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Datawhale 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 数据及背景
  • 2. 文本表示
    • 2.1 词嵌入
      • 2.2 Word2vec
        • 2.2.1 CBOW 模型
        • 2.2.2 Skip-Gram 模型
        • 2.2.3 Negative Sampling
        • 2.2.4 Hierarchical Softmax
        • 2.2.5 Word2Vec存在的问题
        • 2.2.6 FastText 和 Word2vec 中的层次 softmax 区别
        • 2.2.7 Word2vec实践代码
    • 3. 文本分类
      • 3.1 TextCNN
        • 3.1.1 维度分析
        • 3.1.2 ModelEmbedding 类细节
        • 3.1.3 Dataset 类细节
        • 3.1.4 GetLoader 类& collate_fn 细节:
        • 3.1.5 TextCNN 类细节
      • 3.2 TextRNN
        • 3.2.1 维度分析
        • 3.2.2 LSTM复习
        • 3.2.3 TextRNN代码实践
        • 3.2.4 TextRNN实践结果
        • 3.2.5 变长序列
        • 3.2.6 TextCNN与TextRNN 总结
        • 3.2.7 得分总结
      • 3.3 HAN
        • 3.3.1 结构
        • 3.3.2 维度分析
        • 3.3.3 Attention
        • 3.3.4 HAN代码
        • 3.3.5 数据预处理
        • 3.3.6 其他细节
    相关产品与服务
    NLP 服务
    NLP 服务(Natural Language Process,NLP)深度整合了腾讯内部的 NLP 技术,提供多项智能文本处理和文本生成能力,包括词法分析、相似词召回、词相似度、句子相似度、文本润色、句子纠错、文本补全、句子生成等。满足各行业的文本智能需求。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档