TextCNN文本分类实践

CNN介绍

CNN是特殊的全连接层,包含两个特性:平移不变性和局部性(只和周边位置相关)

CNN用于处理图像,也可以处理文本和语音。

处理图像时,基础的CNN不能解决图像旋转、放大、缩小问题,因为神经网络输入是一行像素转换成向量,所以大小不同的同一张图片对于神经网络的输入是不一样的。

常见的操作是卷积和池化(可以省略),卷积核的不同决定了视野范围的不同,pool有max,mean,相当于去除了奇数行和列, 之后再进行flatten,softmax操作

传统文本分类方法

1.训练集(文本预处理)
2.特征工程

文本表示:词袋模型(one-hot),TF-IDF,TextRank
常用特征:
Bag of Words词袋法:
此向量表示法不会保存原始句子中词的顺序
在这里插入图片描述

TF-IDF:
在这里插入图片描述
文档可以看作段落,陌生词可以用一个符号表示,如UNK

3.分类器
分类器:SVM,朴素贝叶斯,LR等

深度学习 文本文类方法

在这里插入图片描述
word embedding:
word2vec训练词向量算法:CBOW SKIP-GRAM
GLOVE无监督
fasttext可以做文本分类和词向量训练,有监督任务学习,反向传播更新embedding矩阵

TextCNN简介

将卷积神经网络CNN应用到文本分类任务,利用多个不同大小的kernel来提取句子中的关键信息(类似于多窗口大小的ngram),从而能更好捕捉局部相关性
在这里插入图片描述
第二层可以理解为某个词,2gram 3gram 4gram
在这里插入图片描述

目标:对数据进行而分类,判断是否两句话是同一个含义

模型关键代码

在这里插入图片描述

BaseModel 重写nn.module
线性及全连接层(out_chanels大写,提取到的特征会更多)
在这里插入图片描述
在这里插入图片描述

TEXTCNN模型全部代码:

import torch.nn as nn
import torch
from torch.utils.data import Dataset, DataLoader
import torch.nn.functional as F

import torch
import torch.autograd as autograd
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as data
import torch.nn.functional as F
import argparse
import pickle
from torch.utils.data import Dataset, DataLoader
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib as mpl


# 参数
class Config(object):
    def __init__(self,
                 word_embedding_dimension=100,
                 word_num=20000,#20000,train 1-19015
                 epoch=5,
                 sentence_max_size=61,#40,59
                 cuda=False,
                 label_num=5,
                 learning_rate=0.03,
                 batch_size=256,
                 out_channel=200 # acc: 0.4994
                 ):
        self.word_embedding_dimension = word_embedding_dimension     # 词向量的维度
        self.word_num = word_num
        self.epoch = epoch                                           # 遍历样本次数
        self.sentence_max_size = sentence_max_size                   # 句子长度
        self.label_num = label_num                                   # 分类标签个数
        self.lr = learning_rate
        self.batch_size = batch_size
        self.out_channel=out_channel
        self.cuda = cuda

config = Config()

class TextDataset(data.Dataset):
    def __init__(self,train_data_list,label_list):
        self.set = torch.tensor(train_data_list)
        self.label = torch.tensor(label_list)
    def __getitem__(self, index):
        return (self.set[index], self.label[index])

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


class TextCNN(nn.Module):
    def __init__(self, config):
        super(TextCNN, self).__init__()
        self.config = config
        self.out_channel = config.out_channel
        self.conv3 = nn.Conv2d(1, 1, (2, config.word_embedding_dimension))
        self.conv4 = nn.Conv2d(1, 1, (3, config.word_embedding_dimension))
        self.conv5 = nn.Conv2d(1, 1, (4, config.word_embedding_dimension))
        self.Max3_pool = nn.MaxPool2d((self.config.sentence_max_size - 2 + 1, 1))
        self.Max4_pool = nn.MaxPool2d((self.config.sentence_max_size - 3 + 1, 1))
        self.Max5_pool = nn.MaxPool2d((self.config.sentence_max_size - 4 + 1, 1))
        self.linear1 = nn.Linear(3, config.label_num)
        self.dropout = nn.Dropout(p=0.5)  # dropout训练
        self.softmax = nn.Softmax()

    def forward(self, x):
        batch = x.shape[0]
        # Convolution
        x1 = F.relu(self.conv3(x))
        x2 = F.relu(self.conv4(x))
        x3 = F.relu(self.conv5(x))

        # Pooling
        x1 = self.Max3_pool(x1)
        x2 = self.Max4_pool(x2)
        x3 = self.Max5_pool(x3)

        # capture and concatenate the features
        x = torch.cat((x1, x2, x3), -1)
        x = x.view(batch, 1, -1)

        x = self.dropout(x)

        # project the features to the labels
        x = self.linear1(x)
        x = x.view(-1, self.config.label_num)
        return F.softmax(x, dim=1)  # F.softmax(self.softmax(x), dim=1)  #


# 自定义token
class Token(object):
    def __init__(self, vocab_file_path, max_len=202):
        self.vocab_file_path = vocab_file_path
        self.max_len = max_len
        self.word2id, self.id2word = self._load_vovab_file()  # 得到词典
        # 进行参数验证
        if self.max_len > 510:  # 表示超过了bert限定长度
            raise Exception(print('设置序列最大长度超过bert限制长度,建议设置max_len<=510'))

    # 加载词表生成word2id和id2word列表
    def _load_vovab_file(self):
        with open(self.vocab_file_path, 'r', encoding='utf-8') as fp:
            vocab_list = [i.replace('\n', '') for i in fp.readlines()]
            word2id = {}
            id2word = {}
            for index, i in enumerate(vocab_list):
                word2id[i] = index
                id2word[index] = i
        return word2id, id2word

    # 定义数据编码encode并生成pytorch所需的数据格式
    def encode_str(self, txt_list: list):
        # 针对所有的输入数据进行编码
        return_txt_id_list = []
        for txt in txt_list:
            inner_str = txt

            inner_str_list = list(inner_str)
            inner_seq_list=[]
            for char in inner_str_list:
                char_index = self.word2id.get(char, False)
                if char_index == False:  # 表示该字符串不认识
                    inner_seq_list.append(self.word2id.get('[UNK]'))
                else:
                    inner_seq_list.append(char_index)
            #inner_seq_list.append(self.word2id.get('[SEP]'))  # 跟上结尾token
            # 执行padding操作
            inner_seq_list += [self.word2id.get('[PAD]')] * (self.max_len - len(inner_str_list))
            return_txt_id_list.append(inner_seq_list)


        return return_txt_id_list#return_data

    # 定义解码操作
    def decode_str(self, index_list):
        return ''.join([self.id2word.get(i) for i in index_list])


def eval_score(preds_prob, label):
    preds = [0 if row[0] > 0.5 else 1 for row in preds_prob]
    pd_data = pd.concat([pd.DataFrame(preds, columns=['预测值']), pd.DataFrame(label, columns=['真实值'])], axis=1)
    # 计算总体的召回率 精准度 f1值
    # loc index,Single label,List of labels.
    total_TP = len(pd_data.loc[(pd_data.真实值 == 1) & (pd_data.预测值 == 1)])  # 真正例
    total_TN = len(pd_data.loc[(pd_data.真实值 == 0) & (pd_data.预测值 == 0)])  # 真反例
    total_FN = len(pd_data.loc[(pd_data.真实值 == 1) & (pd_data.预测值 == 0)])  # 假反例
    total_FP = len(pd_data.loc[(pd_data.真实值 == 0) & (pd_data.预测值 == 1)])  # 假正例
    # if total_TP + total_TP == 0:  # 分母可能为0修改错误
    if total_TP + total_FP == 0:  # 分母可能为0
        total_precision = 0
    else:
        total_precision = total_TP / (total_TP + total_FP)
    if total_TP + total_FN == 0:
        total_recall = 0
    else:
        total_recall = total_TP / (total_TP + total_FN)
    if total_recall + total_precision == 0:
        total_f1 = 0
    else:
        total_f1 = total_recall * total_precision / (total_recall + total_precision)
    # 计算正确率
    acc = len(pd_data.loc[pd_data.真实值 == pd_data.预测值]) / len(pd_data)
    return (total_precision, total_recall, total_f1, acc), ['精准度', '召回率', 'f1值', '正确率']

def get_dataloader(data_set):
    return DataLoader(dataset=data_set,
               batch_size=config.batch_size,
               num_workers=2)
def loaddata():
    path = 'G:/Pythonfile/project/TextCNN/data/train/'
    df_train = pd.read_table(path + "gaiic_track3_round1_train_20210228.tsv",
                             names=['q1', 'q2', 'label']).fillna("0")  # (100000, 3)
    df_test = pd.read_table(path + 'gaiic_track3_round1_testA_20210228.tsv',
                            names=['q1', 'q2']).fillna("0")  # (25000, 2)
    train_num = int(0.7 * df_train.shape[0])


    label_list = df_train['label'].values
    label_list = [[line] for line in label_list]
    train_data_list = (df_train['q1'] + " 19999 " + df_train['q2']).values#ndarray
    train_data_list = [line.split(' ') for line in train_data_list]#list
    train_data_list = [list(map(int, line)) for line in train_data_list]
    sentence_max_size = 61
    def add_padding(data_list):
        for line_list in data_list:
            if len(line_list) <= sentence_max_size:
                line_list += [0] * (sentence_max_size - len(line_list))
        return data_list

    train_data_list = add_padding(train_data_list)

    train_set = TextDataset(train_data_list[:train_num], label_list[:train_num])
    dev_set = TextDataset(train_data_list[train_num:], label_list[train_num:])

    training_iter = get_dataloader(train_set)
    valid_iter = get_dataloader(dev_set)

    return  training_iter,valid_iter

def load_data(path):
    token = Token(r'C:\Users\LENOVO\Desktop\AI_\bert-base-chinese\vocab.txt')

    df_train = pd.read_csv(path + "\\train.csv").fillna("0")  # (100000, 3)

    #train_data_list =[token.encode_str(i) for i in (df_train['sentence1'] + "н" + df_train['sentence2']).values]
    train_data_list = token.encode_str((df_train['sentence1'] + "н" + df_train['sentence2']).values)
    label_data_list =[[line]for line in df_train['label'].values]#array([0, 0, 0, ..., 1, 1, 0], dtype=int64)
    training_set = TextDataset(train_data_list,label_data_list)
    training_iter = DataLoader(dataset=training_set,
                               batch_size=config.batch_size,
                               num_workers=2)

    df_dev = pd.read_csv(path + '\\dev.csv').fillna("0")  # (25000, 2)
    dev_data_list = token.encode_str((df_dev['sentence1'] + "н" + df_dev['sentence2']).values)
    dev_label_data_list = [[line]for line in df_train['label'].values]
    dev_set = TextDataset(dev_data_list, dev_label_data_list)
    dev_iter = DataLoader(dataset=dev_set,
                            batch_size=config.batch_size,
                            num_workers=2)

    # test_set = TextDataset(df_test)
    # test_iter = DataLoader(dataset=test_set,
    #                         batch_size=config.batch_size,
    #                         num_workers=2)
    return  training_iter,dev_iter


def train_model(training_iter,valid_iter):
    # Create the configuration
    model = TextCNN(config)
    embeds = nn.Embedding(config.word_num, config.word_embedding_dimension)
    if torch.cuda.is_available():
        model.cuda()
        embeds = embeds.cuda()
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.SGD(model.parameters(), lr=config.lr)

    # Train the model

    loss_list =[]
    for epoch in range(config.epoch):  #
        loss_sum = 0
        count = 1
        sum_out = np.array([])
        sum_label = np.array([])

        model.train()  # 防止过拟合
        for i, batch_data in enumerate(training_iter):
            data = batch_data[0]  # torch.Size([256, 60]) #batchsize_250
            label = batch_data[1]  # torch.Size([256, 1])
            if torch.cuda.is_available():  # config.cuda and
                data = data.cuda()
                label = label.cuda()
            input_data = embeds(autograd.Variable(data))
            input_data = input_data.unsqueeze(1)  # [256, 202, 100]-->[256, 1, 202, 100]
            optimizer.zero_grad()
            out = model(input_data)#Expected 4-dimensional input for 4-dimensional weight [1, 1, 3, 100]
            loss = criterion(out,
                             autograd.Variable(label).squeeze())  # out:torch.Size([256, 2]),lable:torch.Size([256, 1])--> torch.Size([256])#autograd.Variable(label.float()))
            loss_sum += loss
            loss.backward()
            optimizer.step()
            count = count + 1

        model.eval()
        for i, batch_data in enumerate(valid_iter):
            data = batch_data[0]  # torch.Size([256, 60]) #batchsize_250
            label = batch_data[1]  # torch.Size([256, 1])
            if torch.cuda.is_available():  # config.cuda and
                data = data.cuda()
                label = label.cuda()
            input_data = embeds(autograd.Variable(data))
            input_data = input_data.unsqueeze(1)  # (256,1,60,100)
            optimizer.zero_grad()
            out = model(input_data)#Expected 4-dimensional input for 4-dimensional weight [1, 1, 3, 100]
            sum_out = out.cpu().detach().numpy() if i == 0 else np.append(sum_out, out.cpu().detach().numpy(),axis=0)
            sum_label = label.cpu().detach().numpy() if i == 0 else np.append(sum_label, label.cpu().detach().numpy(),axis=0)

        print("epoch", epoch+1, end='  ')
        print("train The average loss is: %.5f" % (loss_sum / count))
        loss_list.append((loss_sum / count).cpu().detach().numpy())
        total_res, total_label = eval_score(sum_out, sum_label)
        total_precision, total_recall, total_f1, total_acc = total_res
        print(f'valid的 precision:{total_precision} recall:{total_recall}, f1:{total_f1} acc: {total_acc}')

    print('avery_loss_list',loss_list)
    torch.save(model.state_dict(), './data/params.pkl')
    #torch.save(model, './model/model.pkl')
    print('Model saved successfully')


if __name__ == '__main__':
    path = r'G:\Pythonfile\project\bert-utils\data'
    training_iter,valid_iter = loaddata()#加载数据 该数据已经脱敏过了,类似句子1:1 23 34 454 232 句子二:1 34 43 11  标签: 0
    #training_iter,valid_iter = load_data(path) #加载数据未脱敏,类似‘我想请问借款的日利率计是怎样算的吖?’	‘微粒贷均采用日固定利率,按借款的实际天数计息。已发放贷款的执行利率固定不变,每日利息=剩余未还本金(借款金额-已还金额)x日利率(当前日利率为0.05%),您也可以在微粒贷页面点击借款,输入具体借款金额查看对应利息。回复以下序号获取答案:1:了解微粒贷计息详情2:多笔借款的利息计算3:我需要人工帮助’	0

    train_model(training_iter,valid_iter)
    #model = torch.nn.Module.load_state_dict('./model/params.pkl')#使用反序列化的 state_dict 加载模型的参数字典。
    model = TextCNN(config)
    model = model.load_state_dict(torch.load('./data/params.pkl'))#使用反序列化的 state_dict 加载模型的参数字典。

相关名词介绍

分布式表达

主要可以分为三类:基于矩阵的分布表示、基于聚类的分布表示和基于神经网络的分布表示。核心思想也都由两部分组成:一、选择一种方式描述上下文;二、选择一种模型刻画某个词(下文称“目标词”)与其上下文之间的关系。

词的分布式表示:

  1. 基于矩阵的分布表示
    基于矩阵的分布表示通常又称为分布语义模型,在这种表示下,矩阵中的一行,就成为了对应词的表示,这种表示描述了该词的上下文的分布。由于分布假说认为上下文相似的词,其语义也相似,因此在这种表示下,两个词的语义相似度可以直接转化为两个向量的空间距离。
    常见到的Global Vector 模型( GloVe模型)是一种对“词-词”矩阵进行分解从而得到词表示的方法,属于基于矩阵的分布表示。
  2. 基于聚类的分布表示
  3. 基于神经网络的分布表示,词嵌入( word embedding)
    基于神经网络的分布表示一般称为词向量、词嵌入( word embedding)或分布式表示( distributed representation)。
NLP语言模型

语言模型包括文法语言模型和统计语言模型。统计语言模型把语言(词的序列)看作一个随机事件,并赋予相应的概率来描述其属于某种语言集合的可能性。给定一个词汇集合 V,对于一个由 V 中的词构成的序列S = ⟨w1, · · · , wT ⟩ ∈ Vn,统计语言模型赋予这个序列一个概率P(S),来衡量S 符合自然语言的语法和语义规则的置信度。常见的统计语言模型有N元文法模型(N-gram Model)

Embedding

作用:

  • 在 embedding 空间中查找最近邻,这可以很好的用于根据用户的兴趣来进行推荐。
  • 作为监督性学习任务的输入。
  • 用于可视化不同离散变量之间的关系

引入:
One-hot 编码
首先需要表示的离散或类别变量的总个数 N,然后对于每个变量,用 N-1 个 0 和单个 1 组成的 vector 来表示每个类别。这样做有两个很明显的缺点:

  1. 对于具有非常多类型的类别变量,变换后的向量维数过于巨大,且过于稀疏。
  2. 映射之间完全独立,并不能表示出不同类别之间的关系。
# One Hot Encoding Categoricals
books = ["War and Peace", "Anna Karenina", 
          "The Hitchhiker's Guide to the Galaxy"]
books_encoded = [[1, 0, 0],
                 [0, 1, 0],
                 [0, 0, 1]]
Similarity (dot product) between First and Second = 0
Similarity (dot product) between Second and Third = 0
Similarity (dot product) between First and Third = 0               

虑到这两个问题,表示类别变量的理想解决方案则是我们是否可以通过较少的维度表示出每个类别,并且还可以一定的表现出不同类别变量之间的关系,这也就是 embedding 出现的目的。

改进:
将vector做一些改进:
1、将vector每一个元素由整形改为浮点型,变为整个实数范围的表示;
2、将原来稀疏的巨大维度压缩嵌入到一个更小维度的空间。
为了更好的表示类别实体,可以是用一个 embedding neural network 和 supervised 任务来进行学习训练,以找到最适合的表示以及挖掘其内在联系。

One-hot 编码的最大问题在于其转换不依赖于任何的内在关系,而通过一个监督性学习任务的网络,我们可以通过优化网络的参数和权重来减少 loss 以改善我们的 embedding 表示,loss 越小,则表示最终的向量表示中,越相关的类别,它们的表示越相近。

参考:
NLP公开课七月第四节探索文本分类
word2vec和word embedding有什么区别
Embedding 的理解
万物皆Embedding,从经典的word2vec到深度学习基本操作item2vec

  • 0
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
TextCNN(Convolutional Neural Network)是一种用于文本分类的神经网络模型。它基于卷积神经网络,可以对输入的文本进行特征提取和分类。 TextCNN的输入是一个文本,通常由单词或字符组成的序列。首先,将文本中的每个单词(或字符)表示为一个向量。这可以使用预训练的词向量模型(如Word2Vec)得到,也可以通过随机初始化向量并通过反向传播进行训练得到。 接下来,将这些单词向量输入到一维卷积层中。卷积层是通过滑动窗口在输入序列上进行局部感知,提取特定的文本特征。每个窗口的大小可以根据具体任务和数据集的需求进行调整。卷积操作产生了一系列的特征图,其中每个特征图对应不同的特征。 在卷积层之后,可以使用最大池化(MaxPooling)操作进一步提取最重要的特征。最大池化操作获取特征图中的最大值,以减少特征向量的维度。这样做的好处是提供了一种不变性,即无论特征在文本中的位置如何变化,还是可以被捕捉到。 在进行池化操作之后,将得到的特征向量连接起来,进一步经过全连接层进行分类。全连接层将特征映射到最终的类别概率分布上,可以使用Softmax函数来将输出值转化为概率。 TextCNN模型的训练过程通常使用交叉熵损失函数,并通过反向传播算法进行参数优化。可以使用梯度下降等算法对模型进行训练。 总结来说,TextCNN是一种用于文本分类的深度学习模型。它通过卷积操作和池化操作提取文本的特征,并通过全连接层进行分类。这种模型具有参数少、计算速度快、能够捕捉到文本的局部信息等优势,被广泛应用于自然语言处理领域的文本分类任务中。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值