精华内容
下载资源
问答
  • TextCNN

    千次阅读 2019-11-23 08:00:51
    TextCNN

    TextCNN

    链接: 数据集 提取码: 6cgu

    感觉以前自己写的好辣鸡,引起现在我的极度不适

    TextCNN中有个难点就是一维卷积:Conv1d

    介绍一下pytorch中的nn.Conv1d

    Class torch.nn.Conv1d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True)

    • in_channels(int) – 输入信号的通道。在文本分类中,即为词向量的维度
    • out_channels(int) – 卷积产生的通道。有多少个out_channels,就需要多少个1维卷积
    • kernel_size(int or tuple) -卷积核的尺寸,卷积核的大小为(k,),第二个维度是由in_channels来决定的,所以实际上卷积大小为kernel_size*in_channels
    • stride(int or tuple, optional) - 卷积步长
    • padding (int or tuple, optional)- 输入的每一条边补充0的层数
    • dilation(int or tuple, `optional``) – 卷积核元素之间的间距
    • groups(int, optional) – 从输入通道到输出通道的阻塞连接数
    • bias(bool, optional) - 如果bias=True,添加偏置

    基于神经网络的文本分类

    《Convolutional Neural Networks for Sentence Classification》
    —基于卷积神经网络的句子分类
    作者:Yoon Kim
    单位:New York University
    发表会议及时间:EMNLP 2014

    本篇文章是卷积神经网络作为文本分类的开山之作,极大推动了CNN在NLP领域的发展

    一 论文导读

    1.文本分类的简介
    2.卷积神经网络相关技术

    1.文本分类的简介

    文本分类,指的是给定分类体系,将文本分类到某个或者某几个类别中。
    根据其目标类别的数量,文本分类涵盖了二分类、多分类、多标签分类等常见分类场景。

    文本分类任务是计算机语言学的一个分支,同时也是自然语言处理中最基础的任务。涵盖了新闻主题分类,情感分类,关系分类,意图识别等等,是具有重大的理论意义和应用价值。

    文本分类中的关键问题在于文本表示,文本表示指的是,通过某种方式将自然语言文本编码成计算机可以处理的形式,向量。
    主要有两种方式:1.基于独热的词袋子文本表示,2.基于词嵌入的文本表示
    在这里插入图片描述

    文本分类发展历史
    基于规则的文本分类 --》基于特征的文本分类 --》 基于神经网络的文本分类
    趋势:对文本中的关键信息捕获的更准确

    一 基于规则的文本分类

    基本思想:使用人工编写的特定规则进行分类,一般情况下,含有特定的词语、短语或者模式时就将其判断为相应的类别,是最早和最简单的一种分类方法。

    大体流程:输入文本–》规则匹配–》输出类别

    二 基于特征的文本分类

    基本思想:通过人工设计和提取,例如:词法特征、句法特征等,使用机器学习模型来捕获句子中所蕴涵的关键信息,从而来减少噪声对最终结果的影响。

    以空间向量模型为例:

    1. 使用词袋子模型来表示每个词
    2. 使用词项作为特征项,使用词在文档中的TF-IDF值作为词的权重(特征权重)

    TF-IDF(词频-逆文档频率)算法是一种统计方法,用以评估一字词对于一个文件集或一个语料库中的其中一份文件的重要程度。字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降。

    1. 使用加权求和得到文本表示
    2. 训练一个分类器(LR\SVM )进行文本分类
      在这里插入图片描述

    三 基于神经网络的文本分类

    基本思想:首先将输入的文本进行分词等一系列基础操作,随后将句子中的单词转化为低维的词表示,使用编码器(卷积神经网络、循环神经网络)得到句子表示,最终得到文本的目标类别。

    大体流程:输入文本–》词表示–》编码器–》文本表示–》输出类别

    总结:

    基于规则的文本分类方法:

    优点:易于实现、无需训练数据
    缺点:人工成本较高,效果较差

    基于特征的文本分类方法:

    优点:易于复用、能够进行信息的筛选
    缺点:人工提取特征的成本很高

    基于神经网络的文本分类方法:

    优点:无需人工特征、一般效果较好
    缺点:可解释性差、训练资源消耗大

    2.卷积神经网络相关技术

    卷积神经网络:多层感知机是一种全连接结构,但是全连接的网路会存在一定的冗余,卷积神经网络通过局部连接和权重共享的方法来实现对多层感知机的共享。
    在这里插入图片描述
    具体操作:

    卷积操作

    池化操作:池化本身是一种降采样操作,用于降低维度并保留有效信息。

    1. 减少模型参数,避免过拟合,提高训练速度
    2. 保证特征的位置、旋转、伸缩不变性(cv方向)
    3. 将变长的输入转换为固定长度的输入(NLP方向)

    二 论文精读

    1.论文整体框架
    2.传统和经典的方法
    3.论文提出的模型
    4.实验和结果
    5.讨论和总结

    1.论文整体框架
    0.摘要
    1.引言
    2.模型
    3.数据集和实验设置
    4.实验结果和讨论
    5.结论

    2.传统和经典的方法

    基于规则的文本分类方法:

    优点:易于实现、无需训练数据
    缺点:人工成本较高,效果较差

    基于特征的文本分类方法:

    优点:易于复用、能够进行信息的筛选
    缺点:人工提取特征的成本很高

    基于神经网络的文本分类方法:

    优点:无需人工特征、一般效果较好
    缺点:可解释性差、训练资源消耗大

    3.论文提出的模型

    模型结构
    下面这张图是最重要的,需要学习者准确无误的了解。

    现在再看,emmmm,自己写的解说好垃圾!!!!还是看图就行

    • 分四层,输入层–》卷积层–》池化层–》输出层
    • 输入层是词的向量表示,卷积层提出特征,池化层进程聚合,最后进过全连接和softmax来进行输出文本的类别
    • 输入层,一行代表一个词,有k个值,k也就是embeding词向量的维度,本文中是300,也就是说,一个词用300维的向量表示,一句话按顺序从上到下进行排列。一句话最多有n个词,这里文章中n是64,如果有句子不满64,则用zero padding的方法,即补0
    • 下图略有欺骗性,可以先忽视途中黄色线框,只看红色,红色的卷积核大小为2*6(其实是2*300),但一般会说卷积窗口大小为2,在下图中计算是有错误的,如果红色卷积核的大小为2,步幅为1,则最后应该得到1*8的一维向量,但是图中是1*7的,虽然这无伤大雅。
    • 这时,你如果还在意黄色线框,则可以忽略红色线框进行查看,你就会发现,下图还略显合理,因为窗口大小为3的黄卷积核得出的特征图确实要比红色少一格。
    • 接着我们继续忽略黄色框,只看红色,就发现一维的特征图,经过池化后得到一个标量(但经过多个卷积核组合起来后是一维向量),池化操作涵盖了一句话的所有词,因而这个标量就代表了这句话。
    • 下面就是难点了,一般有困惑就在这里,就像上面说的,既然一个标量就已经代表一句话了,为什么用下图中的1*4的一维向量去进行分类,其实,下图中的输入层别看是两张n*k的二维矩阵,其实代表的是同一句话,同一句话,同一句话,经过不同大小的卷积核得到多个特征图,从而得到多个标量(代表了这句话),然后把这几个标量进行组合得到句向量(为什么要多个卷积核,因为要获取更多的特征)
    • 最后再进行全连接进而进行softmax进行分类
    • 再做补充:随着卷积层的加深,其实可以获取更多句中词与词之间的联系,这也是后来有文章的创新点,这也体现了深度学习的特点,深度学习的最大特点就是深,特别是在卷积神经网络方面,更特别是在图像方面。

    在这里插入图片描述
    一 输入层

    第i个词表示:一个k维实值向量
    在这里插入图片描述

    长度为n的序列表示:
    在这里插入图片描述
    其中的符号为拼接操作,n为最大维度,如果小于n的句子,则补充为全0向量

    二 卷积层

    卷积操作
    在这里插入图片描述
    上述公式表示使用窗口大小为h的滤波器
    得到特征图
    在这里插入图片描述
    即当前滤波器所有输出的拼接

    一个滤波器得到一个结果,通常我们会使用多个滤波器(调整感受野)来捕获多种特征

    三 输出层

    全连接层
    在这里插入图片描述
    将句子向量转化为一个维度和类别相同的数量
    随机失活(dropout)
    在这里插入图片描述
    r是失活向量,以一定概率取值为1,否则取值为0

    4.实验和结果

    数据集
    在这里插入图片描述
    MR:电影评论极性判断数据集(正负极性两个类别)
    SST-1:斯坦福情感分类标准数据集
    SST-2:斯坦福情感二分类数据集
    Subj:主客观判断数据集
    TREC:TREC问题类型数据集
    CR:商品评价性判断数据集
    MOQA: MPQA的观点极性判断数据集

    实验设置:
    在这里插入图片描述
    词向量参数会在训练时进行优化

    实验结果:
    在这里插入图片描述
    结论:

    1. 实验对比了之前基于机器学习和人工特征的方法。整体来看,随机初始化词向量(CNN-rand)的方法性能不好,而使用预训练词向量的方法普遍较好。
    2. CNN-multichannel在小规模的数据集上有更好的表现,体现了一种折中思想,即不希望微调后的词向量距离原始值太远,有允许有一定的变化。

    在这里插入图片描述
    3. 原始词向量训练结果中,bad对应最相近的词为good,因为其上下文语境是极其类似的;在non-static的版本中,bad对应相似的词为terrible,因为在微调的过程中,词向量值发生变化,从而更加贴近数据集
    4. 句子中!最接近较为激进的词汇,如lush等,而‘,’更接近于连接词

    5.讨论和总结

    1. 开创性地使用神经网络来解决文本分类问题
    2. 进行充分的实验分析,探讨词向量对模型性能的影响
    3. 探讨神经网络中的一些操作对当前文本分类任务的影响

    推荐相关论文:

    1. 《A Sensitivity Analysis of Convolutional Nerual Networks for Sentence Classification》
      作者:Ye zhang and Byron C. Wallace
      发表会议与时间 IJCNLP 2017
      较充分探讨了cnn在文本分类任务上的各种情况,实验分析、挑战指导

    三 代码实现

    源代码网址:https://github.com/galsang/CNN-sentence-classification-pytorch

    1.回顾
    2.准备工作
    3.数据预处理
    4.训练模型
    5.测试模型
    6.讨论与总结

    1.回顾
    基于卷积神经网络的文本分类模型(TextCNN)

    1. 词向量分别使用预训练固定、预训练微调和随机初始化三种方案
    2. 分析预训练微调后的词向量和原始预训练词向量的差异
    3. TextCNN:
      卷积模型:使用不同大小的滤波器
      全连接层:dropout

    2.准备工作
    注意要有GPU
    运行代码

    python run.py --gpu 0 --dataset MR --save_mode True
    

    在这里插入图片描述
    在这里插入图片描述
    数据集链接中包含了上诉7个数据集,本实验只采用第一个数据集

    在这里插入图片描述

    3.数据预处理

    3.1 读取数据集MR

    def read_MR():
        data = {}
        x, y = [], []
    
        with open("data/MR/rt-polarity.pos", "r", encoding="utf-8") as f:
            for line in f:            # 逐行添加
                if line[-1] == "\n":
                    line = line[:-1]
                x.append(line.split())
                y.append(1)   # 添加groudtruth
    
        with open("data/MR/rt-polarity.neg", "r", encoding="utf-8") as f:
            for line in f:
                if line[-1] == "\n":
                    line = line[:-1]
                x.append(line.split())
                y.append(0) # 添加groudtruth
    

    3.2划分训练集、验证集、测试集

        x, y = shuffle(x, y)   # shuffle是一种打乱操作,一定程度可以提高精确度,看不懂不重要
        dev_idx = len(x) // 10 * 8
        test_idx = len(x) // 10 * 9
    
        data["train_x"], data["train_y"] = x[:dev_idx], y[:dev_idx]   # 训练集
        data["dev_x"], data["dev_y"] = x[dev_idx:test_idx], y[dev_idx:test_idx]  # 验证集
        data["test_x"], data["test_y"] = x[test_idx:], y[test_idx:]  # 测试集
    
        return data
    

    4.训练模型

    1.加载词向量和处理UNK 和 PAD

      if params["MODEL"] != "rand":
            # load word2vec
            print("loading word2vec...")
            word_vectors = KeyedVectors.load_word2vec_format("GoogleNews-vectors-negative300.bin", binary=True)
    
            wv_matrix = []
            for i in range(len(data["vocab"])):
                word = data["idx_to_word"][i]
                if word in word_vectors.vocab:
                    wv_matrix.append(word_vectors.word_vec(word))
                else:
                    wv_matrix.append(np.random.uniform(-0.01, 0.01, 300).astype("float32"))
    
            # one for UNK and one for zero padding
            wv_matrix.append(np.random.uniform(-0.01, 0.01, 300).astype("float32"))
            wv_matrix.append(np.zeros(300).astype("float32"))
            wv_matrix = np.array(wv_matrix)
            params["WV_MATRIX"] = wv_matrix
    
    

    2.初始化相关参数

    class CNN(nn.Module):
        def __init__(self, **kwargs):
            super(CNN, self).__init__()
    
            self.MODEL = kwargs["MODEL"]  # 模型状态
            self.BATCH_SIZE = kwargs["BATCH_SIZE"] # batch大小
            self.MAX_SENT_LEN = kwargs["MAX_SENT_LEN"]   # 最长句子长度
            self.WORD_DIM = kwargs["WORD_DIM"]  # 词向量维度
            self.VOCAB_SIZE = kwargs["VOCAB_SIZE"]  # 字典大小
            self.CLASS_SIZE = kwargs["CLASS_SIZE"]  # 类别总数
            self.FILTERS = kwargs["FILTERS"]   # 滤波器宽度(感受野)
            self.FILTER_NUM = kwargs["FILTER_NUM"] # 滤波器个数
            self.DROPOUT_PROB = kwargs["DROPOUT_PROB"] #dropout概率
            self.IN_CHANNEL = 1    # 词向量类型数
    

    3.初始化卷积层和全连接层

     # one for UNK and one for zero padding
            self.embedding = nn.Embedding(self.VOCAB_SIZE + 2, self.WORD_DIM, padding_idx=self.VOCAB_SIZE + 1)
            if self.MODEL == "static" or self.MODEL == "non-static" or self.MODEL == "multichannel":
                self.WV_MATRIX = kwargs["WV_MATRIX"]
                self.embedding.weight.data.copy_(torch.from_numpy(self.WV_MATRIX))
                if self.MODEL == "static":
                    self.embedding.weight.requires_grad = False
                elif self.MODEL == "multichannel":
                    self.embedding2 = nn.Embedding(self.VOCAB_SIZE + 2, self.WORD_DIM, padding_idx=self.VOCAB_SIZE + 1)
                    self.embedding2.weight.data.copy_(torch.from_numpy(self.WV_MATRIX))
                    self.embedding2.weight.requires_grad = False
                    self.IN_CHANNEL = 2
    
            for i in range(len(self.FILTERS)):
                conv = nn.Conv1d(self.IN_CHANNEL, self.FILTER_NUM[i], self.WORD_DIM * self.FILTERS[i], stride=self.WORD_DIM)
                setattr(self, f'conv_{i}', conv)
    
            self.fc = nn.Linear(sum(self.FILTER_NUM), self.CLASS_SIZE)
    

    在这里插入图片描述
    4.输入层+卷积层+池化层+输出层

       def get_conv(self, i):
            return getattr(self, f'conv_{i}')
    
        def forward(self, inp):
            x = self.embedding(inp).view(-1, 1, self.WORD_DIM * self.MAX_SENT_LEN)
            if self.MODEL == "multichannel":
                x2 = self.embedding2(inp).view(-1, 1, self.WORD_DIM * self.MAX_SENT_LEN)
                x = torch.cat((x, x2), 1)
    
            conv_results = [
                F.max_pool1d(F.relu(self.get_conv(i)(x)), self.MAX_SENT_LEN - self.FILTERS[i] + 1)
                    .view(-1, self.FILTER_NUM[i])
                for i in range(len(self.FILTERS))]
    
            x = torch.cat(conv_results, 1)
            x = F.dropout(x, p=self.DROPOUT_PROB, training=self.training)
            x = self.fc(x)
    
            return x
    
    

    5.训练部分:

    model = CNN(**params).cuda(params["GPU"])
    
        parameters = filter(lambda p: p.requires_grad, model.parameters())
        optimizer = optim.Adadelta(parameters, params["LEARNING_RATE"])
        criterion = nn.CrossEntropyLoss() # 使用交叉熵损失函数
    
        pre_dev_acc = 0
        max_dev_acc = 0
        max_test_acc = 0
        for e in range(params["EPOCH"]):
            data["train_x"], data["train_y"] = shuffle(data["train_x"], data["train_y"])
    
            for i in range(0, len(data["train_x"]), params["BATCH_SIZE"]):
                batch_range = min(params["BATCH_SIZE"], len(data["train_x"]) - i)
                # 数据读取
                batch_x = [[data["word_to_idx"][w] for w in sent] +
                           [params["VOCAB_SIZE"] + 1] * (params["MAX_SENT_LEN"] - len(sent))
                           for sent in data["train_x"][i:i + batch_range]]
                batch_y = [data["classes"].index(c) for c in data["train_y"][i:i + batch_range]]
    
                batch_x = Variable(torch.LongTensor(batch_x)).cuda(params["GPU"])
                batch_y = Variable(torch.LongTensor(batch_y)).cuda(params["GPU"])
    
                optimizer.zero_grad()
                model.train()
                pred = model(batch_x)
                loss = criterion(pred, batch_y)
                loss.backward()
                nn.utils.clip_grad_norm(parameters, max_norm=params["NORM_LIMIT"]) # 计算损失并回传
                optimizer.step()
    
            dev_acc = test(data, model, params, mode="dev")
            test_acc = test(data, model, params)
            print("epoch:", e + 1, "/ dev_acc:", dev_acc, "/ test_acc:", test_acc)
    
            if params["EARLY_STOPPING"] and dev_acc <= pre_dev_acc:
                print("early stopping by dev_acc!")
                break
            else:
                pre_dev_acc = dev_acc
    
            if dev_acc > max_dev_acc:   # 根据验证集来选择模型
                max_dev_acc = dev_acc
                max_test_acc = test_acc
                best_model = copy.deepcopy(model)
    
        print("max dev acc:", max_dev_acc, "test acc:", max_test_acc)
    

    6.模型测试:词转为ID 、模型测试、 统计结果

    def test(data, model, params, mode="test"):
        model.eval()
    
        if mode == "dev":
            x, y = data["dev_x"], data["dev_y"]
        elif mode == "test":
            x, y = data["test_x"], data["test_y"]
    
        x = [[data["word_to_idx"][w] if w in data["vocab"] else params["VOCAB_SIZE"] for w in sent] +
             [params["VOCAB_SIZE"] + 1] * (params["MAX_SENT_LEN"] - len(sent))
             for sent in x]
    
        x = Variable(torch.LongTensor(x)).cuda(params["GPU"])
        y = [data["classes"].index(c) for c in y]
    
        pred = np.argmax(model(x).cpu().data.numpy(), axis=1)
        acc = sum([1 if p == y else 0 for p, y in zip(pred, y)]) / len(pred)
    
        return acc
    

    5.测试模型

    在这里插入图片描述
    在这里插入图片描述

    5.讨论和总结
    在这里插入图片描述

    四 问题思索

    五 代码详解

    model.py

    '''
    
    模型文件
    
    '''
    
    
    '''
    代码讲解前期准备:
    1.一维卷积操作
    class torch.nn.Conv1d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True)
    
        in_channels(int) – 输入信号的通道。在文本分类中,即为词向量的维度
        out_channels(int) – 卷积产生的通道。有多少个out_channels,就需要多少个1维卷积
        kernel_size(int or tuple) - 卷积核的尺寸,卷积核的大小为(k,),第二个维度是由in_channels来决定的,所以实际上卷积大小为kernel_size*in_channels
        stride(int or tuple, optional) - 卷积步长
        padding (int or tuple, optional)- 输入的每一条边补充0的层数
        dilation(int or tuple, `optional``) – 卷积核元素之间的间距
        groups(int, optional) – 从输入通道到输出通道的阻塞连接数
        bias(bool, optional) - 如果bias=True,添加偏置
    
    2.Python setattr() 函数:用于设置属性值
    
                描述
                setattr() 函数对应函数 getattr(),用于设置属性值,该属性不一定是存在的。
                
                语法
                setattr() 语法:
                
                setattr(object, name, value)
                参数
                object -- 对象。
                name -- 字符串,对象属性。
                value -- 属性值。
                返回值
                无。
                
                实例
                以下实例展示了 setattr() 函数的使用方法:
                
                对已存在的属性进行赋值:
                
                >>>class A(object):
                ...     bar = 1
                ... 
                >>> a = A()
                >>> getattr(a, 'bar')          # 获取属性 bar 值
                1
                >>> setattr(a, 'bar', 5)       # 设置属性 bar 值
                >>> a.bar
                5
                如果属性不存在会创建一个新的对象属性,并对属性赋值:
                
                >>>class A():
                ...     name = "runoob"
                ... 
                >>> a = A()
                >>> setattr(a, "age", 28)
                >>> print(a.age)
                28
                >>>
    '''
    import torch
    import torch.nn as nn
    import torch.nn.functional as F
    
    class CNN(nn.Module):
        # 初始定义该模型的超参数等模型参数
        # 这些参数的具体设计在run.py中
        def __init__(self, **kwargs):
            super(CNN, self).__init__()
            self.MODEL = kwargs["MODEL"]  # 模型状态
            self.BATCH_SIZE = kwargs["BATCH_SIZE"]  # batch大小
            self.MAX_SENT_LEN = kwargs["MAX_SENT_LEN"]  # 最长句子长度
            self.WORD_DIM = kwargs["WORD_DIM"]  # 词向量维度
            self.VOCAB_SIZE = kwargs["VOCAB_SIZE"]  # 字典大小
            self.CLASS_SIZE = kwargs["CLASS_SIZE"]  # 类别总数
            self.FILTERS = kwargs["FILTERS"]  # 滤波器宽度(感受野)
            self.FILTER_NUM = kwargs["FILTER_NUM"]  # 滤波器个数
            self.DROPOUT_PROB = kwargs["DROPOUT_PROB"]  # dropout概率
            self.IN_CHANNEL = 1  # 词向量类型数
    
    
    
            # 首先这行代码不重要,为什么会有这个断言,很简单,因为每种滤波器都要有滤波器
            assert (len(self.FILTERS) == len(self.FILTER_NUM))
    
    
            # one for UNK and one for zero padding
            # 词嵌入
            self.embedding = nn.Embedding(self.VOCAB_SIZE + 2, self.WORD_DIM, padding_idx=self.VOCAB_SIZE + 1)
    
            # MODEl代表的是词向量的三种初始化方案,static代表的是词向量为预训练固定;non-static为预训练微调;multichannel折中,这种适合小规模数据集
            if self.MODEL == "static" or self.MODEL == "non-static" or self.MODEL == "multichannel":
                self.WV_MATRIX = kwargs["WV_MATRIX"]
                self.embedding.weight.data.copy_(torch.from_numpy(self.WV_MATRIX))
                if self.MODEL == "static":
                    self.embedding.weight.requires_grad = False
                elif self.MODEL == "multichannel":
                    self.embedding2 = nn.Embedding(self.VOCAB_SIZE + 2, self.WORD_DIM, padding_idx=self.VOCAB_SIZE + 1)
                    self.embedding2.weight.data.copy_(torch.from_numpy(self.WV_MATRIX))
                    self.embedding2.weight.requires_grad = False
                    self.IN_CHANNEL = 2
    
            for i in range(len(self.FILTERS)):
                # nn.Conv1d为一维卷积操作,所以conv就是卷积之后的值,即特征图,setattr即是赋值操作
                conv = nn.Conv1d(self.IN_CHANNEL, self.FILTER_NUM[i], self.WORD_DIM * self.FILTERS[i], stride=self.WORD_DIM)
                setattr(self, f'conv_{i}', conv)
            # fc是全连接操作,FILTER_NUM是滤波器个数对应着句向量的维度,CLASS_SIZE是类别数
            self.fc = nn.Linear(sum(self.FILTER_NUM), self.CLASS_SIZE)
            
        # 得到卷积特征图
        def get_conv(self, i):
            return getattr(self, f'conv_{i}')
        # 前向传播,最后得到类别数x
        def forward(self, inp):
            x = self.embedding(inp).view(-1, 1, self.WORD_DIM * self.MAX_SENT_LEN)
            if self.MODEL == "multichannel":
                x2 = self.embedding2(inp).view(-1, 1, self.WORD_DIM * self.MAX_SENT_LEN)
                x = torch.cat((x, x2), 1)
    
            conv_results = [
                F.max_pool1d(F.relu(self.get_conv(i)(x)), self.MAX_SENT_LEN - self.FILTERS[i] + 1)
                    .view(-1, self.FILTER_NUM[i])
                for i in range(len(self.FILTERS))]
    
            x = torch.cat(conv_results, 1)
            x = F.dropout(x, p=self.DROPOUT_PROB, training=self.training)
            x = self.fc(x)
    
            return x
    
    

    run.py

    from model import CNN
    import utils
    
    from torch.autograd import Variable
    import torch
    import torch.optim as optim
    import torch.nn as nn
    
    from sklearn.utils import shuffle
    from gensim.models.keyedvectors import KeyedVectors
    import numpy as np
    import argparse
    import copy
    
    
    def train(data, params):
        if params["MODEL"] != "rand":
            # load word2vec
            print("loading word2vec...")
            word_vectors = KeyedVectors.load_word2vec_format("GoogleNews-vectors-negative300.bin", binary=True)
    
            wv_matrix = []
            for i in range(len(data["vocab"])):
                word = data["idx_to_word"][i]
                if word in word_vectors.vocab:
                    wv_matrix.append(word_vectors.word_vec(word))
                else:
                    wv_matrix.append(np.random.uniform(-0.01, 0.01, 300).astype("float32"))
    
            # one for UNK and one for zero padding
            wv_matrix.append(np.random.uniform(-0.01, 0.01, 300).astype("float32"))
            wv_matrix.append(np.zeros(300).astype("float32"))
            wv_matrix = np.array(wv_matrix)
            params["WV_MATRIX"] = wv_matrix
    
        model = CNN(**params).cuda(params["GPU"])
    
        parameters = filter(lambda p: p.requires_grad, model.parameters())
        optimizer = optim.Adadelta(parameters, params["LEARNING_RATE"])
        criterion = nn.CrossEntropyLoss()
    
        pre_dev_acc = 0
        max_dev_acc = 0
        max_test_acc = 0
        for e in range(params["EPOCH"]):
            data["train_x"], data["train_y"] = shuffle(data["train_x"], data["train_y"])
    
            for i in range(0, len(data["train_x"]), params["BATCH_SIZE"]):
                batch_range = min(params["BATCH_SIZE"], len(data["train_x"]) - i)
    
                batch_x = [[data["word_to_idx"][w] for w in sent] +
                           [params["VOCAB_SIZE"] + 1] * (params["MAX_SENT_LEN"] - len(sent))
                           for sent in data["train_x"][i:i + batch_range]]
                batch_y = [data["classes"].index(c) for c in data["train_y"][i:i + batch_range]]
    
                batch_x = Variable(torch.LongTensor(batch_x)).cuda(params["GPU"])
                batch_y = Variable(torch.LongTensor(batch_y)).cuda(params["GPU"])
    
                optimizer.zero_grad()
                model.train()
                pred = model(batch_x)
                loss = criterion(pred, batch_y)
                loss.backward()
                nn.utils.clip_grad_norm_(parameters, max_norm=params["NORM_LIMIT"])
                optimizer.step()
    
            dev_acc = test(data, model, params, mode="dev")
            test_acc = test(data, model, params)
            print("epoch:", e + 1, "/ dev_acc:", dev_acc, "/ test_acc:", test_acc)
    
            if params["EARLY_STOPPING"] and dev_acc <= pre_dev_acc:
                print("early stopping by dev_acc!")
                break
            else:
                pre_dev_acc = dev_acc
    
            if dev_acc > max_dev_acc:
                max_dev_acc = dev_acc
                max_test_acc = test_acc
                best_model = copy.deepcopy(model)
    
        print("max dev acc:", max_dev_acc, "test acc:", max_test_acc)
        return best_model
    
    
    def test(data, model, params, mode="test"):
        model.eval()
    
        if mode == "dev":
            x, y = data["dev_x"], data["dev_y"]
        elif mode == "test":
            x, y = data["test_x"], data["test_y"]
    
        x = [[data["word_to_idx"][w] if w in data["vocab"] else params["VOCAB_SIZE"] for w in sent] +
             [params["VOCAB_SIZE"] + 1] * (params["MAX_SENT_LEN"] - len(sent))
             for sent in x]
    
        x = Variable(torch.LongTensor(x)).cuda(params["GPU"])
        y = [data["classes"].index(c) for c in y]
    
        pred = np.argmax(model(x).cpu().data.numpy(), axis=1)
        acc = sum([1 if p == y else 0 for p, y in zip(pred, y)]) / len(pred)
    
        return acc
    
    
    def main():
        parser = argparse.ArgumentParser(description="-----[CNN-classifier]-----")
        parser.add_argument("--mode", default="train", help="train: train (with test) a model / test: test saved models")
        parser.add_argument("--model", default="rand", help="available models: rand, static, non-static, multichannel")
        parser.add_argument("--dataset", default="TREC", help="available datasets: MR, TREC")
        parser.add_argument("--save_model", default=False, action='store_true', help="whether saving model or not")
        # parser.add_argument("--early_stopping", default=False, action='store_true', help="whether to apply early stopping")
        parser.add_argument('--early_stopping', dest='early_stopping', action='store_true', help='whether to apply early stopping')
        parser.add_argument('--no-early_stopping', dest='early_stopping', action='store_false')
        parser.set_defaults(early_stopping=False)
        parser.add_argument("--epoch", default=100, type=int, help="number of max epoch")
        parser.add_argument("--learning_rate", default=1.0, type=float, help="learning rate")
        parser.add_argument("--gpu", default=-1, type=int, help="the number of gpu to be used")
    
        options = parser.parse_args()
        data = getattr(utils, f"read_{options.dataset}")()
    
        data["vocab"] = sorted(list(set([w for sent in data["train_x"] + data["dev_x"] + data["test_x"] for w in sent])))
        data["classes"] = sorted(list(set(data["train_y"])))
        data["word_to_idx"] = {w: i for i, w in enumerate(data["vocab"])}
        data["idx_to_word"] = {i: w for i, w in enumerate(data["vocab"])}
    
        params = {
            "MODEL": options.model,
            "DATASET": options.dataset,
            "SAVE_MODEL": options.save_model,
            "EARLY_STOPPING": options.early_stopping,
            "EPOCH": options.epoch,
            "LEARNING_RATE": options.learning_rate,
            "MAX_SENT_LEN": max([len(sent) for sent in data["train_x"] + data["dev_x"] + data["test_x"]]),
            "BATCH_SIZE": 50,
            "WORD_DIM": 300,
            "VOCAB_SIZE": len(data["vocab"]),
            "CLASS_SIZE": len(data["classes"]),
            "FILTERS": [3, 4, 5],
            "FILTER_NUM": [100, 100, 100],
            "DROPOUT_PROB": 0.5,
            "NORM_LIMIT": 3,
            "GPU": options.gpu
        }
    
        print("=" * 20 + "INFORMATION" + "=" * 20)
        print("MODEL:", params["MODEL"])
        print("DATASET:", params["DATASET"])
        print("VOCAB_SIZE:", params["VOCAB_SIZE"])
        print("EPOCH:", params["EPOCH"])
        print("LEARNING_RATE:", params["LEARNING_RATE"])
        print("EARLY_STOPPING:", params["EARLY_STOPPING"])
        print("SAVE_MODEL:", params["SAVE_MODEL"])
        print("=" * 20 + "INFORMATION" + "=" * 20)
    
        if options.mode == "train":
            print("=" * 20 + "TRAINING STARTED" + "=" * 20)
            model = train(data, params)
            if params["SAVE_MODEL"]:
                utils.save_model(model, params)
            print("=" * 20 + "TRAINING FINISHED" + "=" * 20)
        else:
            model = utils.load_model(params).cuda(params["GPU"])
    
            test_acc = test(data, model, params)
            print("test acc:", test_acc)
    
    
    if __name__ == "__main__":
        main()
    
    

    utils.py

    '''
    
    工具文件:数据的预处理,存储和加载数据
    
    '''
    
    from sklearn.utils import shuffle
    import pickle
    # 读取数据TREC
    def read_TREC():
        data = {}
    
        def read(mode):
            x, y = [], []
    
            with open("data/TREC/TREC_" + mode + ".txt", "r", encoding="utf-8") as f:
                for line in f:
                    if line[-1] == "\n":
                        line = line[:-1]
                    y.append(line.split()[0].split(":")[0])
                    x.append(line.split()[1:])
    
            x, y = shuffle(x, y)
    
            if mode == "train":
                dev_idx = len(x) // 10
                data["dev_x"], data["dev_y"] = x[:dev_idx], y[:dev_idx]
                data["train_x"], data["train_y"] = x[dev_idx:], y[dev_idx:]
            else:
                data["test_x"], data["test_y"] = x, y
    
        read("train")
        read("test")
    
        return data
    
    # 读取数据MR
    def read_MR():
        data = {}
        x, y = [], []
    
        with open("data/MR/rt-polarity.pos", "r", encoding="utf-8") as f:
            for line in f:
                if line[-1] == "\n":
                    line = line[:-1]
                x.append(line.split())
                y.append(1)
    
        with open("data/MR/rt-polarity.neg", "r", encoding="utf-8") as f:
            for line in f:
                if line[-1] == "\n":
                    line = line[:-1]
                x.append(line.split())
                y.append(0)
    
        x, y = shuffle(x, y)
        dev_idx = len(x) // 10 * 8
        test_idx = len(x) // 10 * 9
    
        data["train_x"], data["train_y"] = x[:dev_idx], y[:dev_idx]
        data["dev_x"], data["dev_y"] = x[dev_idx:test_idx], y[dev_idx:test_idx]
        data["test_x"], data["test_y"] = x[test_idx:], y[test_idx:]
    
        return data
    
    # 保存模型
    def save_model(model, params):
        path = f"saved_models/{params['DATASET']}_{params['MODEL']}_{params['EPOCH']}.pkl"
        pickle.dump(model, open(path, "wb"))
        print(f"A model is saved successfully as {path}!")
    
    # 加载模型
    def load_model(params):
        path = f"saved_models/{params['DATASET']}_{params['MODEL']}_{params['EPOCH']}.pkl"
    
        try:
            model = pickle.load(open(path, "rb"))
            print(f"Model in {path} loaded successfully!")
    
            return model
        except:
            print(f"No available model such as {path}.")
            exit()
    
    
    
    展开全文
  • TEXTCNN

    2020-08-05 15:32:20
    TEXTCNN出自论文《Convolutional Neural Networks for Sentence Classification》中,作者将CNN网络用于句子级别的文本分类。 原文中TEXTCNN网络结构图如图所示: 首先,将n个映射为词向量的单词链接成一句话,即:...

    TEXTCNN出自论文《Convolutional Neural Networks for Sentence Classification》中,作者将CNN网络用于句子级别的文本分类。
    原文中TEXTCNN网络结构图如图所示:
    在这里插入图片描述
    首先,将n个映射为词向量的单词链接成一句话,即:
    在这里插入图片描述
    在卷积操作中,作者采用了多个不同高度的卷积核进行卷积操作,即:
    在这里插入图片描述
    从而形成特征映射c:
    在这里插入图片描述
    在c上采用1-max池化操作得出这个卷积核过滤器的特征向量cˇ=max(c)\check{c}=max(c).由于作者采用多个不同高度的卷积核(多种、每种多个)会得到多个ciˇ\check{c_i},即:
    在这里插入图片描述
    最后将z送入全连接的softmax层,得出句子的标签概率分布。
    另外作者在网络结构的倒数第二层采用了dropout思想进行正则化来防止过拟合。在这里插入图片描述
    更为直观的图:
    在这里插入图片描述
    另外在词向量嵌入阶段,作者采用了四种方式:
    CNN-rand:作者在模型中随机初始化词向量,再在模型训练中进行微调修改。
    CNN-static:作者采用预训练好的词向量,并且在模型训练中保持不变。
    CNN-non-static:作者采用预训练好的词向量,并且在模型训练中进行微调。
    CNN-multichannel:作者采用两个不同的预训练好的词向量,视为两个通道,但是梯度只通过一个通道进行传播,这样可以保持一组词向量不变,另一组微调。

    代码块

    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.vocab_size, config.embed_size)
            # 卷积层
            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)
            out = out.type(torch.FloatTensor).cuda()
            out = out.transpose(1, 2).contiguous()
            out = torch.cat([self.conv_and_pool(out, conv) for conv in self.convs], 1)
            out = self.dropout(out)
            out = self.fc(out)
            return out
    
    展开全文
  • textcnn

    千次阅读 2019-11-01 16:45:56
    textcnn可选三种卷积核,每种卷积核默认128,给的图式是两个,其中输入嵌入矩阵的行为文档的单词数,列为单词的嵌入矩阵的大小,选一个batch即一个batch的文档数。用的时候扩展到四维,第四维是1。 学习的参数就是...

    参考链接
    textcnn可选三种卷积核,每种卷积核默认128,给的图式是两个,其中输入嵌入矩阵的行为文档的单词数,列为单词的嵌入矩阵的大小,选一个batch即一个batch的文档数。用的时候扩展到四维,第四维是1。

    学习的参数就是卷积核的维度即2✖️单词的维度,3✖️单词的维度,4✖️单词的维度,另外每个还有一个b,即卷积层学习的参数是((2+3+4)✖️单词的维度 + 3)✖️卷积核数128。每个卷积操作要经过一个relu激活函数,之后才会最大池。

    到了全连接层之前经过一个dropout,全连接层会对128✖️3=384的全连接层(行向量,用x的转置表示),学习一个W矩阵,大小为384✖️2(2分类),这样就可以x的转置✖️W+B就变成2分类了,此时的B是1✖️2的b,之后接一个softmax进行归一化。

    学习的是上述参数矩阵,但是可以对输入词嵌入矩阵进行微调,让更好的学习上述的参数矩阵

    展开全文
  • textCNN

    2019-07-03 22:03:57
    然而研究证明,TextCnn在文本分类问题上有着更加卓越的表现。 TextCnn的结构 1. 嵌入层(embedding layer) textcnn使用预先训练好的词向量作embedding layer。对于数据集里的所有词,因为每个词都可以表征成一...

    https://blog.csdn.net/John_xyz/article/details/79210088   原理

    https://www.cnblogs.com/jiangxinyang/p/10207482.html   实现

    https://blog.csdn.net/loseinvain/article/details/78935192   卷积 tf.nn.conv2d()使用

     

    原理
    paper地址:https://arxiv.org/pdf/1408.5882.pdf 
    对于文本分类问题,常见的方法无非就是抽取文本的特征,比如使用doc2evc或者LDA模型将文本转换成一个固定维度的特征向量,然后在基于抽取的特征训练一个分类器。 
    然而研究证明,TextCnn在文本分类问题上有着更加卓越的表现。

    TextCnn的结构
    1. 嵌入层(embedding layer)
    textcnn使用预先训练好的词向量作embedding layer。对于数据集里的所有词,因为每个词都可以表征成一个向量,因此我们可以得到一个嵌入矩阵MM, MM里的每一行都是词向量。这个MM可以是静态(static)的,也就是固定不变。可以是非静态(non-static)的,也就是可以根据反向传播更新

    2. 卷积池化层(convolution and pooling)
    卷积(convolution)
    输入一个句子,首先对这个句子进行切词,假设有ss个单词。对每个词,跟句嵌入矩阵MM, 可以得到词向量。假设词向量一共有dd维。那么对于这个句子,便可以得到ss行dd列的矩阵AϵRs×dAϵRs×d. 
    我们可以把矩阵AA看成是一幅图像,使用卷积神经网络去提取特征。由于句子中相邻的单词关联性总是很高的,因此可以使用一维卷积。卷积核的宽度就是词向量的维度dd,高度是超参数,可以设置。 
    现在假设有一个卷积核,是一个宽度为dd,高度为hh的矩阵ww,那么ww有h∗dh∗d个参数需要被更新。对于一个句子,经过嵌入层之后可以得到矩阵AϵRs×dAϵRs×d. A[i:j]A[i:j]表示AA的第ii行到第jj行, 那么卷积操作可以用如下公式表示:

    oi=w⋅A[i:i+h−1]oi=w⋅A[i:i+h−1] ,i=1,2,...,s−h+1,i=1,2,...,s−h+1
    叠加上偏置bb,在使用激活函数ff激活, 得到所需的特征。公式如下: 
    ci=f(oi+b)ci=f(oi+b)
    对一个卷积核,可以得到特征cϵRs−h+1cϵRs−h+1, 总共s−h+1s−h+1个特征。我们可以使用更多高度不同的卷积核,得到更丰富的特征表达。

    池化(pooling)
    不同尺寸的卷积核得到的特征(feature map)大小也是不一样的,因此我们对每个feature map使用池化函数,使它们的维度相同。最常用的就是1-max pooling,提取出feature map照片那个的最大值。这样每一个卷积核得到特征就是一个值,对所有卷积核使用1-max pooling,再级联起来,可以得到最终的特征向量,这个特征向量再输入softmax layer做分类。这个地方可以使用drop out防止过拟合

    以上过程可以用下图直观表示

    这里word embedding的维度是5。对于句子 i like this movie very much。可以转换成如上图所示的矩阵AϵR7×5AϵR7×5
    有6个卷积核,尺寸为(2×5)(2×5), (3×5)(3×5), 4×54×5,每个尺寸各2个.
    AA分别与以上卷积核进行卷积操作,再用激活函数激活。每个卷积核都得到了特征向量(feature maps)
    使用1-max pooling提取出每个feature map的最大值,然后在级联得到最终的特征表达。
    将特征输入至softmax layer进行分类, 在这层可以进行正则化操作( l2-regulariation)

     


    3. 参数选择
    根据文章中的描述,总结如下。感兴趣的同学请仔细阅读原文 
    1. 初始化词向量 
    使用word2vec和golve都可以,不要使用one-hot vectors 
    2. 卷积核的尺寸 
    1-10之间,具体情况具体分析,对最终结果影响较大。一般来讲,句子长度越长,卷积核的尺寸越大。 
    3. 每种尺寸卷积核的数量 
    100-600之间,对模型性能影响较大,需要注意的是增加卷积核的数量会增加训练模型的实践。 
    4. 激活函数的选择 
    使用relu函数 
    5. drop out rate 
    0.0-0.5, 当增加卷积核的数量时,可以尝试增加drop out rate,甚至可以大于0.5 
    6. 池化的选择 
    1-max pooling 
    7. 正则项 
    正则项对最终模型性能的影响很小

    基于tensorflow的代码实现
    # coding: utf-8
    import pickle
    import logging
    import tensorflow as tf
    logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s',level=logging.INFO)

    class TextCNN(object):
        """
        A CNN for text classification.
        Uses an embedding layer, followed by a convolution, max-pooling and soft-max layer.
        """
        def __init__(self, config):
            self.lr = config['lr']
            self.batch_size = config['batch_size']
            # 词典的大小
            self.vocab_size = config['vocab_size']
            self.num_classes = config['num_classes']
            self.keep_prob = config['keep_prob']
            # length of word embedding
            self.embedding_size = config['embedding_size']
            # seting filter sizes, type of list
            self.filter_sizes = config['filter_sizes']
            # max length of sentence
            self.sentence_length = config['sentence_length']
            # number of filters
            self.num_filters = config['num_filters']

        def add_placeholders(self):
            self.X = tf.placeholder('int32', [None, self.sentence_length])
            self.y = tf.placeholder('int32', [None, ])

        def inference(self):
            with tf.variable_scope('embedding_layer'):
                # loading embedding weights
                with open('Text_cnn/embedding_matrix.pkl','rb') as f:
                    embedding_weights = pickle.load(f)
                # non-static 
                self.W = tf.Variable(embedding_weights, trainable=True, name='embedding_weights',dtype='float32')
                # shape of embedding chars is (None, sentence_length, embedding_size)
                self.embedding_chars = tf.nn.embedding_lookup(self.W, self.X)
                # shape of embedding char expanded is (None, sentence_length, embedding_size, 1)
                self.embedding_chars_expanded = tf.expand_dims(self.embedding_chars, -1)
            with tf.variable_scope('convolution_pooling_layer'):
                pooled_outputs = []
                for i, filter_size in enumerate(self.filter_sizes):
                    filter_shape = [filter_size, self.embedding_size, 1, self.num_filters]
                    W = tf.get_variable('W'+str(i), shape=filter_shape,
                                        initializer=tf.truncated_normal_initializer(stddev=0.1))
                    b = tf.get_variable('b'+str(i), shape=[self.num_filters],
                                        initializer=tf.zeros_initializer())
                    conv = tf.nn.conv2d(self.embedding_chars_expanded, W, strides=[1,1,1,1],
                                        padding='VALID', name='conv'+str(i))
                    # apply nonlinearity
                    h = tf.nn.relu(tf.add(conv, b))
                    # max pooling
                    pooled = tf.nn.max_pool(h, ksize=[1, self.sentence_length - filter_size + 1, 1, 1],
                        strides=[1, 1, 1, 1], padding='VALID', name="pool")
                    # shape of pooled is (?,1,1,300)
                    pooled_outputs.append(pooled)
                # combine all the pooled features
                self.feature_length = self.num_filters * len(self.filter_sizes)
                self.h_pool = tf.concat(pooled_outputs,3)
                # shape of (?, 900)
                self.h_pool_flat = tf.reshape(self.h_pool, [-1, self.feature_length])
            # add dropout before softmax layer
            with tf.variable_scope('dropout_layer'):
                # shape of [None, feature_length]
                self.features = tf.nn.dropout(self.h_pool_flat, self.keep_prob)
            # fully-connection layer
            with tf.variable_scope('fully_connection_layer'):
                W = tf.get_variable('W', shape=[self.feature_length, self.num_classes],
                                    initializer=tf.contrib.layers.xavier_initializer())
                b = tf.get_variable('b', shape=[self.num_classes],
                                    initializer=tf.constant_initializer(0.1))
                # shape of [None, 2]
                self.y_out = tf.matmul(self.features, W) + b
                self.y_prob = tf.nn.softmax(self.y_out)

        def add_loss(self):
            loss = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=self.y, logits=self.y_out)
            self.loss = tf.reduce_mean(loss)
            tf.summary.scalar('loss',self.loss)

        def add_metric(self):
            self.y_pred = self.y_prob[:,1] > 0.5
            self.precision, self.precision_op = tf.metrics.precision(self.y, self.y_pred)
            self.recall, self.recall_op = tf.metrics.recall(self.y, self.y_pred)
            # add precision and recall to summary
            tf.summary.scalar('precision', self.precision)
            tf.summary.scalar('recall', self.recall)

        def train(self):
            # Applies exponential decay to learning rate
            self.global_step = tf.Variable(0, trainable=False)
            # define optimizer
            optimizer = tf.train.AdamOptimizer(self.lr)
            extra_update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
            with tf.control_dependencies(extra_update_ops):
                self.train_op = optimizer.minimize(self.loss, global_step=self.global_step)

        def build_graph(self):
            """build graph for model"""
            self.add_placeholders()
            self.inference()
            self.add_loss()
            self.add_metric()
            self.train()

    展开全文

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,370
精华内容 548
关键字:

textcnn