基于CNN的文本分类

   

目录

前言

一、论文笔记

二、基于pytorch的文本预处理

      1、读取数据集

      2、构建词表

      3、将文字转换成数字特征

      4、将每条文本转换为数字列表

      5、将每条文本设置为相同长度

      6、将每个词编码转换为词向量

三、基于pytorch的TextCNN模型的构建

      1、模型构建

四、训练模型

     1、训练模型的基本步骤

     2、每个一定的batch就查看验证集的情况

     3、一定的正则化手段(早停:连续1000batch验证集数据没有提升,就停止训练)

     4、测试数据

五、绘制TextCNN模型结构图

    1、绘制模型图

六、pytorch模型的保存与加载           


 

前言

深度学习模型在计算机视觉语音识别方面取得了卓越的成就,在 NLP 领域也是可以的。将卷积神经网络CNN应用到文本分类任务,利用多个不同size的kernel来提取句子中的关键信息(类似 n-gram 的关键信息,从而能够更好地捕捉局部相关性。

一、论文笔记

1、Yoon Kim在2014年 “Convolutional Neural Networks for Sentence Classification” 论文中提出TextCNN(利用卷积神经网络对文本进行分类的算法)(该论文翻译)。

 

 

 假设我们有一些句子需要对其进行分类。句子中每个词是由n维词向量组成的,也就是说输入矩阵大小为m*n,其中m为句子长度。CNN需要对输入样本进行卷积操作,对于文本数据,filter不再横向滑动,仅仅是向下移动,有点类似于N-gram在提取词与词间的局部相关性。图中共有三种步长策略,分别是2,3,4,每个步长都有两个filter(实际训练时filter数量会很多)。在不同词窗上应用不同filter,最终得到6个卷积后的向量。然后对每一个向量进行最大化池化操作并拼接各个池化值,最终得到这个句子的特征表示,将这个句子向量丢给分类器进行分类,至此完成整个流程。

(1)嵌入层(Embedding Layer)

通过一个隐藏层, 将 one-hot 编码的词投影到一个低维空间中,本质上是特征提取器,在指定维度中编码语义特征。 这样, 语义相近的词, 它们的欧氏距离或余弦距离也比较近。(作者使用的单词向量是预训练的,方法为fasttext得到的单词向量,当然也可以使用word2vec和GloVe方法训练得到的单词向量)。

(2)卷积层(Convolution Laye)

在处理图像数据时,CNN使用的卷积核的宽度和高度的一样的,但是在text-CNN中,卷积核的宽度是与词向量的维度一致!这是因为我们输入的每一行向量代表一个词,在抽取特征的过程中,词做为文本的最小粒度。而高度和CNN一样,可以自行设置(通常取值2,3,4,5),高度就类似于n-gram了。由于我们的输入是一个句子,句子中相邻的词之间关联性很高,因此,当我们用卷积核进行卷积时,不仅考虑了词义而且考虑了词序及其上下文(类似于skip-gram和CBOW模型的思想)。

(3)池化层(Pooling Layer)

因为在卷积层过程中我们使用了不同高度的卷积核,使得我们通过卷积层后得到的向量维度会不一致,所以在池化层中,我们使用1-Max-pooling对每个特征向量池化成一个值,即抽取每个特征向量的最大值表示该特征,而且认为这个最大值表示的是最重要的特征。当我们对所有特征向量进行1-Max-Pooling之后,还需要将每个值给拼接起来。得到池化层最终的特征向量。在池化层到全连接层之前可以加上dropout防止过拟合。

(4)全连接层(Fully connected layer)

全连接层跟其他模型一样,假设有两层全连接层,第一层可以加上’relu’作为激活函数,第二层则使用softmax激活函数得到属于每个类的概率。

(5)TextCNN的小变种

在词向量构造方面可以有以下不同的方式: CNN-rand: 随机初始化每个单词的词向量通过后续的训练去调整。 CNN-static: 使用预先训练好的词向量,如word2vec训练出来的词向量,在训练过程中不再调整该词向量。 CNN-non-static: 使用预先训练好的词向量,并在训练过程进一步进行调整。 CNN-multichannel: 将static与non-static作为两通道的词向量。

(6)参数与超参数

sequence_length (Q: 对于CNN, 输入与输出都是固定的,可每个句子长短不一, 怎么处理? A: 需要做定长处理, 比如定为n, 超过的截断, 不足的补0. 注意补充的0对后面的结果没有影响,因为后面的max-pooling只会输出最大值,补零的项会被过滤掉)
num_classes (多分类, 分为几类)
vocabulary_size (语料库的词典大小, 记为|D|)
embedding_size (将词向量的维度, 由原始的 |D| 降维到 embedding_size)
filter_size_arr (多个不同size的filter)


2、2015年“A Sensitivity Analysis of (and Practitioners' Guide to) Convolutional Neural Networks for Sentence Classification”论文详细地阐述了关于TextCNN模型的调参心得。

 

(1)TextCNN详细过程:

Embedding:第一层是图中最左边的7乘5的句子矩阵,每行是词向量,维度=5,这个可以类比为图像中的原始像素点。
Convolution:然后经过 kernel_sizes=(2,3,4) 的一维卷积层,每个kernel_size 有两个输出 channel。
MaxPolling:第三层是一个1-max pooling层,这样不同长度句子经过pooling层之后都能变成定长的表示。
FullConnection and Softmax:最后接一层全连接的 softmax 层,输出每个类别的概率。
(2)论文调参结论:

~使用预训练的word2vec 、 GloVe初始化效果会更好。一般不直接使用One-hot。
~卷积核的大小影响较大,一般取1~10,对于句子较长的文本,则应选择大一些。
~卷积核的数量也有较大的影响,一般取100~600 ,同时一般使用Dropout(0~0.5)。
~激活函数一般选用ReLU 和 tanh。
~池化使用1-max pooling。
~随着feature map数量增加,性能减少时,试着尝试大于0.5的Dropout。
~评估模型性能时,记得使用交叉验证。
 

二、基于pytorch的文本预处理

      1、读取数据集

        

with open(file_path, 'r', encoding='UTF-8') as f:

      2、构建词表

def build_vocab(file_path, tokenizer, max_size, min_freq):
    vocab_dic = {}
    with open(file_path, 'r', encoding='UTF-8') as f:
        for line in tqdm(f):
            lin = line.strip()
            if not lin:
                continue
            content = lin.split('\t')[0]
            for word in tokenizer(content):
                vocab_dic[word] = vocab_dic.get(word, 0) + 1 #词频
        vocab_list = sorted([_ for _ in vocab_dic.items() if _[1] >= min_freq], key=lambda x: x[1], reverse=True)[:max_size]#可以将输入的文本中的每个词编号,编号是根据词频的,词频越大,编号越小。
        vocab_dic = {word_count[0]: idx for idx, word_count in enumerate(vocab_list)} #每个词对应的编码和词频
        vocab_dic.update({UNK: len(vocab_dic), PAD: len(vocab_dic) + 1})#对UNK进行编号
    return vocab_dic

      3、将文字转换成数字特征

      4、将每条文本转换为数字列表

      5、将每条文本设置为相同长度

def build_dataset(config,use_word):
    if use_word:
        tokenizer = lambda x:x.split(' ') #以空格隔开,word-level
    else:
        tokenizer = lambda x:[y for y in x]#char-level
    if os.path.exists(config.vocab_path):   #config.vocab_path ='THUCNews/data/vocab.pkl'
        vocab = pkl.load(open(config.vocab_path,'rb'))
    else:
        vocab = build_vocab(config.train_path,#'THUCNews/data/train.txt'
                            tokenizer=tokenizer,#tokenizer = lambda x: [y for y in x] tokenizer目前还是一个函数,把参数传进去,返回的是个列表
                            max_size=MAX_VOCAB_SIZE,
                            min_freq=1
                            )
        pkl.dump(vocab,open(config.vocab_path,'wb'))
    print(f"Vocab size:{len(vocab)}")

    def load_dataset(path,pad_size=32):
        contents = []
        with open(path,'r',encoding='UTF-8') as f:
            for line in tqdm(f):
                lin = line.strip()
                if not lin:
                    continue
                content,label = lin.split('\t')
                words_line = []
                token = tokenizer(content)#使用Tokenizer将文字转换成数字特征
                seq_len = len(token)
                #由于每句话的长度不唯一,需要将每句话的长度设置一个固定值。将超过 
                  #固定值的部分截掉,不足的在最前面用0填充。
                if pad_size:
                    if len(token) < pad_size:
                        token.extend([PAD]*(pad_size-len(token)))
                    else:
                        token = token[:pad_size]
                        seq_len=pad_size
                #word to id
                for word in token:
                    words_line.append(vocab.get(word,vocab.get(UNK)))#将每条文本转变成一个向量
                contents.append((words_line,int(label),seq_len))
        return contents  # [([...], 0), ([...], 1), ...]
    train = load_dataset(config.train_path, config.pad_size)
    dev = load_dataset(config.dev_path, config.pad_size)
    test = load_dataset(config.test_path, config.pad_size)
    return vocab, train, dev, test

     6、构建batches

class DatasetIterater(object):
    def __init__(self,batches,batch_size,device):
        self.batch_size = batch_size
        self.batches = batches
        self.n_batches = len(batches) // batch_size
        self.residue = False#记录batch数量是否为整数
        if len(batches) % self.n_batches != 0:
            self.residue = True #不是整数
        self.index =0
        self.device = device

    def _to_tensor(self,datas):
        x = torch.LongTensor([_[0] for _ in datas]).to(self.device)
        y = torch.LongTensor([_[1] for _ in datas]).to(self.device)
        #pad前的长度(超过pad_size的设为pad_size)
        seq_len = torch.LongTensor([_[2] for _ in datas]).to(self.device)
        return (x,seq_len),y
    def __next__(self):
        if self.residue and self.index == self.n_batches:
            batches = self.batches[self.index * self.batch_size: len(self.batches)]
            self.index += 1
            batches = self._to_tensor(batches)
            return batches
        elif self.index >= self.n_batches:
            self.index = 0
            raise  StopIteration
        else:
            batches = self.batches[self.index* self.batch_size :(self.index+1)*self.batch_size]
            self.index += 1
            batches = self._to_tensor(batches)
            return batches

    def __iter__(self):
        return self
    def __len__(self):
        if self.residue:
            return self.n_batches+1
        else:
            return self.n_batches

def build_iterator(dataset,config):
    iter = DatasetIterater(dataset,config.batch_size,config.device)
    return iter

      7、将每个词编码转换为词向量

Embedding层基于上文所得的词编码,对每个词进行one-hot编码,每个词都会是一个vocabulary_size维的向量;然后通过神经网络的训练迭代更新得到一个合适的权重矩阵(具体实现过程可以参考skip-gram模型),行大小为vocabulary_size,列大小为词向量的维度,将本来以one-hot编码的词向量映射到低维空间,得到低维词向量。需要声明一点的是Embedding层是作为模型的第一层,在训练模型的同时,得到该语料库的词向量。当然,也可以使用已经预训练好的词向量表示现有语料库中的词。
这里没有用训练好的词向量

if __name__ == "__main__":
    '''提取预训练词向量'''
    # 下面的目录、文件名按需更改。
    train_dir = "./THUCNews/data/train.txt"
    vocab_dir = "./THUCNews/data/vocab.pkl"
    pretrain_dir = "./THUCNews/data/sgns.sogou.char"
    emb_dim = 300
    filename_trimmed_dir = "./THUCNews/data/embedding_SougouNews"
    if os.path.exists(vocab_dir):
        word_to_id = pkl.load(open(vocab_dir, 'rb'))
    else:
        # tokenizer = lambda x: x.split(' ')  # 以词为单位构建词表(数据集中词之间以空格隔开)
        tokenizer = lambda x: [y for y in x]  # 以字为单位构建词表
        word_to_id = build_vocab(train_dir, tokenizer=tokenizer, max_size=MAX_VOCAB_SIZE, min_freq=1)
        pkl.dump(word_to_id, open(vocab_dir, 'wb'))

    embeddings = np.random.rand(len(word_to_id), emb_dim)
    f = open(pretrain_dir, "r", encoding='UTF-8')
    for i, line in enumerate(f.readlines()):
        # if i == 0:  # 若第一行是标题,则跳过
        #     continue
        lin = line.strip().split(" ")#以空格为分隔符
        if lin[0] in word_to_id:#如果
            idx = word_to_id[lin[0]]
            emb = [float(x) for x in lin[1:301]]
            embeddings[idx] = np.asarray(emb, dtype='float32')
    f.close()
    np.savez_compressed(filename_trimmed_dir, embeddings=embeddings)

文本预处理目的:将每个样本转换为一个数字矩阵,矩阵的每一行表示一个词向量。

三、基于pytorch的TextCNN模型的构建

      1、模型构建

class Config(object):
    """配置参数"""
    def __init__(self,dataset,embedding):
         # dataset = 'THUCNews', embedding='random'
         self.model_name ='TextCNN'
         self.train_path = dataset+'/data/train.txt'
         self.dev_path = dataset + '/data/dev.txt'
         self.test_path = dataset +'/data/test.txt'
         self.class_list = [x.strip() for x in open(
             dataset + '/data/class.txt',encoding='utf-8').readlines()]
         self.vocab_path = dataset + '/data/vocab.pkl'
         self.save_path = dataset + '/saved_dict/'+self.model_name+'ckpt'
         self.embedding_pretrained = torch.tensor(
             np.load(dataset+'/data/'+embedding)["embedding"].astype('float32')
              ) if embedding !='random' else None #预训练词向量
         self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') #设备
         self.dropout =0.5 # 随机失活
         self.require_improvement =1000# 若超过1000batch效果还没提升,则提前结束训练
         self.num_classes = len(self.class_list)#类别数
         self.n_vocab = 0 #词表大小在运行时赋予值
         self.num_epochs = 20
         self.batch_size = 128 #mini-batch的大小
         self.pad_size =32#每句话处理成的长度
         self.learning_rate =1e-3#学习率
         self.embed = self.embedding_pretrained.size(1)\
              if self.embedding_pretrained is not None else 300#字向量维度
         # [vocab_size,x] vocab_size 是词表的大小 x是维度 (用x维的向量表示一个词)
         self.filter_sizes =(2,3,4)#卷积核尺寸
         self.num_filters = 256#卷积核数量(channels数)


class Model(nn.Module):
    def __init__(self,config):
        super(Model,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,padding_idx=config.n_vocab - 1)

        self.convs = nn.ModuleList(
            [nn.Conv2d(1,config.num_filters,(k,config.embed)) for k in config.filter_sizes])
        self.dropout =nn.Dropout(config.dropout)
        self.fc =nn.Linear(config.num_filters * len(config.filter_sizes),config.num_classes)

    def conv_and_pool(self,x,conv):
        x = F.relu(conv(x)).squeeze(3) #(batch_size,out_filters,pad_size-k+1)
        x = F.max_pool1d(x,x.size(2)).squeeze(2)#(batch_size,out_filters)
        return x

    def forward(self,x): #x->(batch_size,n_vocab)
        out=self.embedding(x[0]) #(batch_size,pad_size,embedding_dim)
        out= out.unsqueeze(1) #(batch_size,1,pad_size,embedding_dim)
        out=torch.cat([self.conv_and_pool(out,conv) for conv in self.convs] ,1)#(batch_size,out_filters*len(filter_sizes))
        out=self.dropout(out)
        out=self.fc(out) #
        return out #(batch_size,num_classes)

四、训练模型

     1、训练模型的基本步骤

     2、每个一定的batch就查看验证集的情况

     3、一定的正则化手段(早停:连续1000batch验证集数据没有提升,就停止训练)

     4、测试数据

def train(config,model,train_iter,dev_iter,test_iter):
    start_time =time.time()
    model.train()
    optimizer = torch.optim.Adam(model.parameters(),lr=config.learning_rate)

    #学习率指数衰减,每次epoch:学习率=gamma*学习率
    #scheduler = torch.optim.lr_scheduler.ExponentialLR(optimizer,gamma=0.9)

    total_batch = 0#记录进行到多少batch
    dev_best_loss = float('inf')
    last_improve = 0 #记录是否很久没有效果提升
    writer = SummaryWriter(log_dir = config.log_path + '/' + time.strftime('%m-%d_%H.%M',time.localtime()))
    #前向传播得到预测值 --> 求预测值与真实值的损失 -->优化器梯度清零 --> 求所有参数的梯度  --> 优化器更新梯度
    for epoch in range(config.num_epochs):
        print('Epoch [{}/{}]'.format(epoch+1,config.num_epochs))
        #scheduler.stop()#学习率衰减
        for i, (trains,labels) in enumerate(train_iter):
            outputs = model(trains)
            model.zero_grad()
            loss = F.cross_entropy(outputs,labels)
            loss.backward()
            optimizer.step()
            if total_batch % 100 ==0:
                #每多少轮输出在训练集和验证集上的效果
                true = labels.data.cpu()
                #torch.max(outputs.data,1)这个函数返回的是两个值,第一个值是具体的value(我们用下划线_表示),第二个值是value所在的index(也就是predicted)。dim=1表示输出所在行的最大值,若改写成dim=0则输出所在列的最大值
                predic = torch.max(outputs.data,1)[1].cpu() ##outputs->(batch_size,num_classes)
                train_acc = metrics.accuracy_score(true,predic)#计算此时模型在验证集上的损失和准确率
                if dev_loss < dev_best_loss:
                    dev_best_loss = dev_loss  # 更新验证集最小损失
                    torch.save(model.state_dict(), config.save_path)  # 保存在验证集上损失最小的参数
                    improve = '*'  # 效果提升标志
                    last_improve = total_batch  # 计算上次提升 位于哪个batch
                else:
                    improve = ''
                time_dif = get_time_dif(start_time)
                msg = 'Iter: {0:>6},  Train Loss: {1:>5.2},  Train Acc: {2:>6.2%},  Val Loss: {3:>5.2},  Val Acc: {4:>6.2%},  Time: {5} {6}'
                print(msg.format(total_batch, loss.item(), train_acc, dev_loss, dev_acc, time_dif, improve))
                # 保存 训练集(当前batch)、验证集的损失和准确率信息 方便可视化 以batch为单位
                writer.add_scalar("loss/train", loss.item(), total_batch)
                writer.add_scalar("loss/dev", dev_loss, total_batch)
                writer.add_scalar("acc/train", train_acc, total_batch)
                writer.add_scalar("acc/dev", dev_acc, total_batch)
                model.train()  # 回到训练模式
            total_batch += 1
            if total_batch - last_improve > config.require_improvement:  # 如果长期没有提高 就提前终止
                # 验证集loss超过1000batch没下降,结束训练
                print("No optimization for a long time, auto-stopping...")
                flag = True
                break
        if flag:
            break
    writer.close()
    test(config,model,test_iter)


def evaluate(config, model, data_iter, test=False):
    model.eval()  # 测试模式
    loss_total = 0
    predict_all = np.array([], dtype=int)  # 存储验证集所有batch的预测结果
    labels_all = np.array([], dtype=int)  # 存储验证集所有batch的真实标签
    with torch.no_grad():
        for texts, labels in data_iter:
            outputs = model(texts)
            loss = F.cross_entropy(outputs, labels)
            loss_total += loss
            labels = labels.data.cpu().numpy()
            predic = torch.max(outputs.data, 1)[1].cpu().numpy()
            labels_all = np.append(labels_all, labels)
            predict_all = np.append(predict_all, predic)

    acc = metrics.accuracy_score(labels_all, predict_all)  # 计算验证集准确率
    if test:  # 如果是测试集的话 计算一下分类报告和混淆矩阵
        report = metrics.classification_report(labels_all, predict_all, target_names=config.class_list, digits=4)
        confusion = metrics.confusion_matrix(labels_all, predict_all)  # 计算混淆矩阵
        return acc, loss_total / len(data_iter), report, confusion
    return acc, loss_total / len(data_iter)  # 返回准确率和每个batch的平均损失

def test(config, model, test_iter):
    # test
    model.load_state_dict(torch.load(config.save_path)) #加载使验证集损失最小的参数
    model.eval() #测试模式
    start_time = time.time()
    test_acc, test_loss, test_report, test_confusion = evaluate(config, model, test_iter, test=True) #计算测试集准确率,每个batch的平均损失 分类报告、混淆矩阵
    msg = 'Test Loss: {0:>5.2},  Test Acc: {1:>6.2%}'
    print(msg.format(test_loss, test_acc))
    print("Precision, Recall and F1-Score...")
    print(test_report)
    print("Confusion Matrix...")
    print(test_confusion)
    time_dif = get_time_dif(start_time)
    print("Time usage:", time_dif)

五、绘制TextCNN模型结构图

    1、绘制模型图

 

六、pytorch模型的保存与加载     

(50条消息) TextCNN文本分类(keras实现)_Asia-Lee的博客-CSDN博客_textcnn文本分类

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值