TextCNN(二)

TextCNN

1.论文结构

2.TextCNN 结构

TextCNN的结果不复杂,第一层是输入层把原始的词转换为向量表示,然后接下来是卷积提取不同层次的特征,并用的是Rule激活函数,池化层对卷积层的结果做Max pooling, 最后经过全连接和softmax进行输出得到句子的类别。

具体的参数设置参考文献《A Sensitivity Analysis of (and Practitioners’ Guide to) Convolutional Neural Networks for Sentence Classificatio》

多通道结构

这里提到了,词向量模型用了两个channels, 一个是静态, 一个是动态。

  • 静态的意思是在反向传播到词向量这里, 词向量不变,词向量不变意味着不进行反向传播。
  • 动态的意思是在反向传播到词向量这里,词向量微调,词向量微调意味着进行反向传播。
模型的正则化
  • Dropout:抓爆

在神经网络的传播过程中,让某个神经元以一定的概率p停止工作,从而增加模型的泛化能力

  • L2-正则

3.实验结果与分析

MR:电影评论极性判断数据集
SST-1:斯坦福情感分类标准数据集(5类:(very positive, positive, neutral, negative, very negative)
SST-2:斯坦福情感二分类数据集
Subj:主客观判断数据集
TREC:TREC问题类型数据集
CR:商品(cameras, MP3s etc.)评价极性判断数据集
MPQA:MPQA的观点极性判断数据集
下表中,c代表分类数量
l应该是句子的最大长度
N是数据集大小
V是词表大小
Vpre是在预训练词表中出现的单词数量
test表示是否划分验证集,如果没有划分则采用CV(cross valid交叉验证的方式划分)

4.论文总结

关键点
·预训练的词向量—-Word2Vec、Glove
·卷积神经网络结构——一维卷积、池化层
·超参选择——卷积核选择、词向量方式选择
创新点
·提出了基于CNN的文本分类模型TextCNN
·提出了多种词向量设置方式
·在四个文本分类任务上取得最优的结果
·对超参进行大量实验和分析
开创性地使用神经网络结合预训练的词向量来解决文本分类问题
进行充分的实验分析,探讨词向量对模型性能的影响
探讨神经网络中的一些操作对当前任务的影响

5.代码实现

准备工作

项目环境配置
·Python3.5
·jupyter notebook
·torch 1.4.0
·numpy 1.16.2
·gensim 3.8.1用来读取预训练词向量
·torchsummary 1.5.1

数据集下载

MR数据集https:///www.cs.cornell.edu/people/pabo/movie-review-data/

预训练词向量word2vec下载:https:/pan.baidu.com/s/1jJ9eAaE
或者:http://pan.baidu.com/s/1kTCQqft
glove下载http://downloads.cs.stanford.edu/nlp/data/glove.840B.300d.zip

数据处理

# coding:utf-8
from torch.utils import data
import os
import random
import numpy as np
import nltk
import torch
from gensim.test.utils import datapath, get_tmpfile
from gensim.models import KeyedVectors

# 总体步骤:
# ·词向量导入
# ·数据集加载
# ·构建word2id并pad成相同长度
# ·求词向量均值和方差
# ·生成词向量
# ·生成训练集、验证集和测试集

# 继承自torch的Dataset,要实现:
# __init__
# __getitem__
# __len__
class MR_Dataset(data.Dataset):
    def __init__(self, state="train", k=0, embedding_type="word2vec"):

        self.path = os.path.abspath('.')
        if "data" not in self.path:
            self.path += "/data"
        # 数据集加载,每行都是一句话
        pos_samples = open(self.path + "/MR/rt-polarity.pos", errors="ignore").readlines()
        neg_samples = open(self.path + "/MR/rt-polarity.neg", errors="ignore").readlines()
        # 把正负样本放在一块
        datas = pos_samples + neg_samples
        # datas = [nltk.word_tokenize(data) for data in datas]
        # 用空格进行分词
        datas = [data.split() for data in datas]
        # 求句子最大长度,将所有句子pad成一样的长度;
        # 有时也可以pad为平均长度,如果使用平均长度,对于超过长度的句子需要截断。
        max_sample_length = max([len(sample) for sample in datas])
        # 为正负样本设置标签
        labels = [1] * len(pos_samples) + [0] * len(neg_samples)
        word2id = {"<pad>": 0}  # 生成word2id,pad对应0
        for i, data in enumerate(datas):
            for j, word in enumerate(data):
                # 词不还没加入word2id中则加入,并设置其id
                if word2id.get(word) is None:
                    word2id[word] = len(word2id)
                # 设置每个句子中所有词,替换为ID
                datas[i][j] = word2id[word]
            # 将句子按最大长度进行pad
            datas[i] = datas[i] + [0] * (max_sample_length - len(datas[i]))
            # 如果是按平均长度则按下面语句进行截断或补齐,max_sample_length代表平均长度
            # datas[i] = datas[i][0:max_sample_length]+[0]*(max_sample_length-len(datas[i]))
        self.n_vocab = len(word2id)
        self.word2id = word2id

        #根据配置取不同的预训练词向量
        if embedding_type == "word2vec":
            self.get_word2vec()
        elif embedding_type == "glove":
            self.get_glove_embedding()
        else:
            pass
        # self.get_word2vec()
        # 由于训练集中的数据前半部分是正样本,后半部分是负样本,需要打乱训练集
        # 把数据和标签放到一起打乱
        c = list(zip(datas, labels))  # 打乱训练集
        random.seed(1)
        random.shuffle(c)
        # 再把数据和标签分开
        datas[:], labels[:] = zip(*c)

        # 生成训练集、验证集和测试集
        # 总的数据分成10份,第k份作为测试集,其他9份再分
        # 其他9分的后10%做为验证集,前90%做为训练集
        if state == "train":  # 生成训练集
            # 取除第k份外其他9份
            self.datas = datas[:int(k * len(datas) / 10)] + datas[int((k + 1) * len(datas) / 10):]
            self.labels = labels[:int(k * len(datas) / 10)] + labels[int((k + 1) * len(labels) / 10):]
            # 取前90%做为训练集
            self.datas = np.array(self.datas[0:int(0.9 * len(self.datas))])
            self.labels = np.array(self.labels[0:int(0.9 * len(self.labels))])
        elif state == "valid":  # 生成验证集
            # 取除第k份外其他9份
            self.datas = datas[:int(k * len(datas) / 10)] + datas[int((k + 1) * len(datas) / 10):]
            self.labels = labels[:int(k * len(datas) / 10)] + labels[int((k + 1) * len(labels) / 10):]
            # 取后10%做为验证集
            self.datas = np.array(self.datas[int(0.9 * len(self.datas)):])
            self.labels = np.array(self.labels[int(0.9 * len(self.labels)):])
        elif state == "test":  # 生成测试集
            # 第k份作为测试集
            self.datas = np.array(datas[int(k * len(datas) / 10):int((k + 1) * len(datas) / 10)])
            self.labels = np.array(labels[int(k * len(datas) / 10):int((k + 1) * len(datas) / 10)])

    def __getitem__(self, index):
        return self.datas[index], self.labels[index]

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

    def get_glove_embedding(self):
        '''
        生成glove词向量
        :return: 根据词表生成词向量
        '''
        if not os.path.exists(self.path + "/glove_embedding_mr.npy"):  # 如果已经保存了词向量,就直接读取
            # 与word2vec不一样的是glove文件是txt格式,要先转换为word2vec格式
            # 这个转换过程比较慢,所以转换好就先保存,下次直接读。
            if not os.path.exists(self.path + "/test_word2vec.txt"):
                glove_file = datapath(self.path + '/glove.840B.300d.txt')
                # 指定转化为word2vec格式后文件的位置
                tmp_file = get_tmpfile(self.path + "/glove_word2vec.txt")
                from gensim.scripts.glove2word2vec import glove2word2vec
                glove2word2vec(glove_file, tmp_file)
            else:
                tmp_file = get_tmpfile(self.path + "/glove_word2vec.txt")
            print("Reading Glove Embedding...")
            # 注意这里的binary=True不用写。
            wvmodel = KeyedVectors.load_word2vec_format(tmp_file)

            # 求词向量均值和方差
            # 论文中提到,用方差对未知词进行初始化对于训练词向量的效果很不错
            tmp = []
            for word, index in self.word2id.items():
                try:
                    tmp.append(wvmodel.get_vector(word))
                except:
                    pass
            mean = np.mean(np.array(tmp))
            std = np.std(np.array(tmp))
            print(mean, std)
            # 用上面的词向量均值和方差来生成词向量
            vocab_size = self.n_vocab
            embed_size = 300
            embedding_weights = np.random.normal(mean, std, [vocab_size, embed_size])  # 正态分布初始化方法
            for word, index in self.word2id.items():
                try:
                    # 如果预训练词向量中有对应的词就使用预训练的词向量,否则就用正态分布初始化的词向量
                    embedding_weights[index, :] = wvmodel.get_vector(word)
                except:
                    pass
            # 由于每次读取这个东西很费时,所以处理好后保存下来,下次直接读取
            np.save(self.path + "/glove_embedding_mr.npy", embedding_weights)  # 保存生成的词向量
        else:
            embedding_weights = np.load(self.path + "/glove_embedding_mr.npy")  # 载入生成的词向量
        self.weight = embedding_weights

    def get_word2vec(self):
        '''
        生成word2vec词向量
        :return: 根据词表生成的词向量
        '''
        if not os.path.exists(self.path + "/word2vec_embedding_mr.npy"):  # 如果已经保存了词向量,就直接读取
            print("Reading word2vec Embedding...")
            # 加载预训练的Word2Vec词向量
            wvmodel = KeyedVectors.load_word2vec_format(self.path + "/GoogleNews-vectors-negative300.bin.gz",
                                                        binary=True)
            tmp = []
            for word, index in self.word2id.items():
                try:
                    tmp.append(wvmodel.get_vector(word))
                except:
                    pass
            mean = np.mean(np.array(tmp))
            std = np.std(np.array(tmp))
            print(mean, std)
            vocab_size = self.n_vocab
            embed_size = 300
            embedding_weights = np.random.normal(mean, std, [vocab_size, embed_size])  # 正太分布初始化方法
            for word, index in self.word2id.items():
                try:
                    embedding_weights[index, :] = wvmodel.get_vector(word)
                except:
                    pass
            np.save(self.path + "/word2vec_embedding_mr.npy", embedding_weights)  # 保存生成的词向量
        else:
            embedding_weights = np.load(self.path + "/word2vec_embedding_mr.npy")  # 载入生成的词向量
        self.weight = embedding_weights


if __name__ == "__main__":
    mr_train_dataset = MR_Dataset()
    print(mr_train_dataset.__len__())
    print(mr_train_dataset[0])
    mr_valid_dataset = MR_Dataset("valid")
    print(mr_valid_dataset.__len__())
    print(mr_valid_dataset[0])
    mr_test_dataset = MR_Dataset("test")
    print(mr_test_dataset.__len__())
    print(mr_test_dataset[0])

模型的构建

# -*- coding: utf-8 -*-
import torch
import torch.autograd as autograd
import torch.nn as nn
import torch.nn.functional as F
from .BasicModule import BasicModule


class TextCNN(BasicModule):

    def __init__(self, config):
        super(TextCNN, self).__init__()
        # 嵌入层
        if config.embedding_pretrained is not None:
            self.embedding = nn.Embedding.from_pretrained(config.embedding_pretrained, freeze=False)
        else:
            self.embedding = nn.Embedding(config.n_vocab, config.embed_size)
        # 卷积层,按config中设置卷积核数量进行设置
        if config.cuda:
            self.convs = [nn.Conv1d(config.embed_size, config.filter_num, filter_size).cuda()
                          for filter_size in config.filters]
        else:
            self.convs = [nn.Conv1d(config.embed_size, config.filter_num, filter_size)
                          for filter_size in config.filters]
        # Dropout层
        self.dropout = nn.Dropout(config.dropout)
        # 分类层
        self.fc = nn.Linear(config.filter_num*len(config.filters), config.label_num)
    def conv_and_pool(self,x,conv):
        x = F.relu(conv(x))
        # 池化层
        x = F.max_pool1d(x,x.size(2)).squeeze(2)
        return x
    def forward(self, x):
        out = self.embedding(x) # batch_size*length*embedding_size
        # 这里把第1个维度和第2个维度交换一下(起始维度编号是0)
        out = out.transpose(1, 2).contiguous() # batch_size*embedding_size*length
        # 这里对所有卷积核做卷积的结果(这里卷积和pooling一起做:conv_and_pool)进行循环
        out = torch.cat([self.conv_and_pool(out, conv) for conv in self.convs], 1) # batch_size*(filter_num*len(filters))
        out = self.dropout(out)
        out = self.fc(out) # batch_size*label_num
        return out


if __name__ == '__main__':
    print('running the TextCNN...')

有卷积核做卷积的结果(这里卷积和pooling一起做:conv_and_pool)进行循环
out = torch.cat([self.conv_and_pool(out, conv) for conv in self.convs], 1) # batch_size*(filter_numlen(filters))
out = self.dropout(out)
out = self.fc(out) # batch_size
label_num
return out

if name == ‘main’:
print(‘running the TextCNN…’)


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值