【AI达人创造营第二期】一文读懂textCNN模型原理
时间:2025-08-09
【AI达人创造营第二期】一文读懂textCNN模型原理
之前我们提到 CNN 时,通常会认为是属于计算机视觉领域。但是,在,Yoon Kim 针对 CNN 的输入层做了一些变形,从而提出了文本分类模型 textCNN。由于在计算机视觉中,常被用于提取图像的局部特征图,并且起到了很好的效果,所以该作者将其引入到 NLP 中,应用于文本分类任务,试图使用 CNN 捕捉文本中单词之间的关系。

一文读懂textCNN模型原理
一、简介
在之前,我们提及卷积神经网络(CNN)时,通常认为它属于计算机视觉领域。然而,这一观点不久便发生了变化,Yoon Kim 针对 CNN 输入层进行了修改,并提出了文本分类模型 textCNN。尽管 CNN 在图像处理中发挥着重要作用,但Kim 的创新在于将其应用到自然语言处理(NLP)领域,以捕捉单词间的相互关系。通过这种重新设计的 CNN 模型,Kim 试图证明其在文本分类任务上的有效性与计算机视觉中的表现相当,从而展示了该技术在 NLP 领域的潜力和可能性。
二、与传统 CNN 的不同

图一-textCNN架构
相较于传统的图像CNN网络,TextCNN在结构上并没有任何改动,反而更加简单和高效。它由一层卷积、一层最大池化以及最终的Softmax分类器组成,完全简化了复杂流程。
然而,TextCNN最大的不同在于其输入数据的不同:图像的数据是二维甚至是三维的,卷积核从左到右、从上到下进行滑动来提取特征;而自然语言则是线性的,经过词向量生成了二维矩阵,但对这些向量按从左到右进行滑动进行卷积并没有实际意义。例如,“今天”对应的向量是[ ,当窗口大小为左到右滑动得到的四个向量分别是[ , [ , [ , [ ,这四组向量分别代表“今天”这个词汇。这种方式虽然进行了向量的变换,但并未产生实质性的信息提取效果。
TextCNN的一大优势在于其简洁的网络结构,在模型结构如此简单的情况下,通过引入预训练好的词向量仍然取得了显著效果。在多项测试数据集上,它的表现超过了基准模型。这种简约设计不仅降低了参数数量和计算负担,还加快了训练速度。在一个配备了vGPU 的单机单卡系统中,只需要 万次训练,经过步迭代后仅需半小时即可收敛。
三、模型架构简析
Convolutional Neural Networks for Sentence Classification一文中最早给出了文本CNN的基本结构,而后A Sensitivity Analysis ...一文专门做了各种控制变量的实验对比。

图二-Convolutional Neural Networks for Sentence Classification模型示意图

图三-A Sensitivity Analysis ...[2]模型示意图
通过将文本语句分解为一系列词,并将其转化为具有k维词向量的格式,最初的模型能够接收的形式是n×k的单通道图像。
之后经过卷积层处理。在卷积层中,使用了不同的卷积核来生成不同的特征图,并且卷积核的宽度 k(即词向量的维度)决定了最终提取到的特征数量。因此,在这里,卷积的作用是用于捕捉不同数量单词之间的关系,比如当卷积核大小为 时,将相邻的三个词语视为一个特征。
在上图一的第二个阶段中,由于选用的卷积核宽度与词向量维度相同,因此最终获得的特征图呈现为一个 n 维向量形式。接下来,对每个特征图执行一次 max-pooling 过程,并将 max-pooling 结果作为全连接层的输入进行处理。最终,这些结果通过 Softmax 层分类。
四、textCNN模型架构
我们仍以图一中文语句为例
(一)Word Embedding 分词构建词向量
如图一所示,textCNN首先将今天天气很好,出来玩分词成“今天/天气/很好/,/出来/玩”,通过wordec或GLOV等嵌入方式,每个词映射为一个的词向量。例如,“今天” → [ , “天气” → [ , “很好” → [ 等。

通过这种方法,我们将自然语言数值化,简化了后续处理过程。从实际效果来看,不同的映射方式对最终结果有着巨大影响,在当前热门的研究领域中,如何将自然语言转换成更优的词向量成为研究热点。在构建好词向量后,我们将这些词向量拼接在一起形成一个* 二维矩阵作为初始输入。
(二)卷积池化层(convolution and pooling)
卷积(convolution)
文本卷积与图像卷积的不同之处在于只针对文本序列的一个方向(垂直)做卷积,即对句子中的每个单词都使用固定宽度的词向量进行处理。特征图由多个特征单元组成,其中每个单元对应一个窗口内的子特征表示,这些特征单元通过卷积操作提取出特定的上下文信息,从而为后续的分类或预测任务提供丰富的语义表示。
现在假设有一个卷积核,是一个宽度为d,高度为h的矩阵w,那么w有hd个参数需要被更新。对于一个句子,经过嵌入层之后可以得到矩阵ARs×d。 A[i:j]表示A的第i行到第j行,卷积操作可以用如下公式表示:\[ w \cdot (AR)_{r}^{(h-} = AR_{}^{(h-} - AR_{}^{(h-}, \]其中,\( A \)是一个具有s个句子的矩阵。

叠加上偏置b,在使用激活函数f激活, 得到所需的特征。公式如下:

具体卷积计算参考博客文本分类算法TextCNN原理详解(一)
对于多通道(channel)的说明
在CNN(卷积神经网络)中,经常提到的词是“channel”。图三展示了深红矩阵与浅红矩阵这两个channel,它们统称为一个卷积核,并且通过每个矩分别对输入进行一次卷积操作,从而得到一个feature map。在计算机视觉领域里,彩色图像由RGB(红色、绿色、蓝色)三种颜色组成,因此每个颜色分别代表一个channel。

根据作者描述,在最初阶段引入通道的主要目的是为了避免过拟合并确保网络对输入信息的学习不会过于精确。然而,后续研究表明,通过正则化同样可以达到类似的效果,从而在较少的数据集上取得更好的预测表现。
不过,使用多个通道相比单通道,每个通道可以采用不同的词嵌入(例如,在非静态(梯度可回传)的通道中进行预训练以优化词嵌入,使它们更符合当前训练需求)。
关于Channel是否适用于TextCNN,根据论文的实验证明,多个通道没有显著提高模型的分类能力,在五组数据集中有四个中单通道的TextCNN表现优于多通道的版本。
最大池化(max-pooling)

得到feamap = [ 后,从中选取一个最大值[作为输出,即为max-pooling。max-pooling不仅保留了主要特征,还大大减少了参数数目。如图五所示,feature map从三维变为一维,其优点体现在两点:一是保持了关键信息;二是降低了复杂度,提高了效率和性能。
经过模型优化,减少了过拟合的可能性;当feature map为[ 或[ 时,最终输出均为[,说明初始输入的细微变化对识别结果没有显著影响。
- 参数减少, 进一步加速计算。
pooling 本身无法带来平移不变性(图片有个字母A, 这个字母A 无论出现在图片的哪个位置, 在CNN的网络中都可以识别出来),卷积核的权值共享才能.
max-pooling的原理是通过从多个值中选取一个最大值来实现降维处理,并且它无法做到平移不变性。而CNN(卷积神经网络)能够实现平移不变性的关键在于,在滑动卷积核的过程中,权值保持固定,即权值共享。这意味着如果训练好的卷积核可以识别字母“A”,那么无论在图像的哪个位置滑动这个卷积核,都能准确地识别出对应的“A”。这种设计使得CNN在网络输入不同大小或者位置时仍然能够保持良好的性能表现。
(三)优化与正则化
在完成池化层之后,我们通常会加入全连接层和SoftMax层来进行分类任务。这些操作后,我们可以得到每个类别(例如label为概率)和另一个类别的概率(如label为-概率)。为了防止过拟合,一般还会采用L则化方法来减少模型复杂度。最后,我们通过梯度下降法更新模型参数以进行优化,从而达到最佳的性能表现。
五、案例与代码实现
在这个案例中,我们参考了飞桨项目THUCNews数据集来完成文本分类任务。THUCNews数据集由清华大学自然语言处理实验室根据新浪新闻RSS订阅频道至间的历史数据筛选并过滤生成的篇新闻文档构成,总大小为GB,全部采用UTF-文本格式存储。为了进一步优化分类任务的表现,我们在原始的新浪新闻分类体系的基础上重新划分了候选分类类别。这些类别包括财经、股票、房产、家居、教育、科技、社会、时尚、时政、体育、星座、游戏和娱乐。通过这样细致的分类方案,我们可以更准确地捕捉到不同领域的新闻内容,并为其提供针对性的分析和服务。在训练过程中,我们采用了TextCNN模型进行文本分类任务,这是一种基于卷积神经网络(Convolutional Neural Network, CNN)的技术。TextCNN能够从文字中提取出关键信息并加以识别和分类,适用于处理具有显著位置特征的信息如新闻标题、摘要等文本数据。总结来说,这个案例通过使用来自THUCNews数据集的高质量数据,并结合TextCNN模型进行分类任务的训练,展示了如何在自然语言处理领域中高效地应用预训练模型来完成复杂的文本分析和分类任务。
(一)环境配置
In [1]
import pandas as pdimport numpy as npimport paddleimport paddle.nn as nnfrom paddle.io import DataLoader, Datasetimport paddle.optimizer as optimfrom paddlenlp.data import Padimport jiebafrom collections import Counter登录后复制
(二)数据准备
In [2]
data = pd.read_csv("data/data45260/Train.txt", sep='\t', names=['ClassNo', 'ClassName', 'Sentence'])print("Train Set")print(data.head())print(data.info()) pred_data = pd.read_csv("data/data45260/Test.txt", sep='\t', names=['Sentence'])print("Pred Set")print(pred_data.head())print(pred_data.info())登录后复制
Train Set ClassNo ClassName Sentence 0 0 财经 上证50ETF净申购突增 1 0 财经 交银施罗德保本基金将发行 2 0 财经 基金公司不裁员反扩军 走访名校揽人才 3 0 财经 基金巨亏30亿 欲打开云天系跌停自救 4 0 财经 基金市场周二缩量走低 <class 'pandas.core.frame.DataFrame'> RangeIndex: 752476 entries, 0 to 752475 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 ClassNo 752476 non-null int64 1 ClassName 752476 non-null object 2 Sentence 752475 non-null object dtypes: int64(1), object(2) memory usage: 17.2+ MB None Pred Set Sentence 0 北京君太百货璀璨秋色 满100省353020元 1 教育部:小学高年级将开始学习性知识 2 专业级单反相机 佳能7D单机售价9280元 3 星展银行起诉内地客户 银行强硬客户无奈 4 脱离中国的实际 强压RMB大幅升值只能是梦想 <class 'pandas.core.frame.DataFrame'> RangeIndex: 83599 entries, 0 to 83598 Data columns (total 1 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Sentence 83599 non-null object dtypes: object(1) memory usage: 653.2+ KB None登录后复制 In [3]
## 注意到训练集中语句与数据数目不匹配,需删除无效数据data.dropna(axis=0, how='any', inplace=True) X = data['Sentence'].values Y = data['ClassNo'].values Y_name = data['ClassName'][np.sort(np.unique(data['ClassName'], return_index=True)[1])].values Y_dict = dict([val for val in zip(np.unique(Y), Y_name)])登录后复制
数据预处理,中文语句的处理需要注意中文分词及停用词的去除。根据提供的中文停用词数据集,将其数据进行加载。然后使用 jieba 包提供的中文分词功能,将训练集的输入语句每一句首先进行分词,调用 jieba.lcut(s) 进行分词并且返回分词后的列表。对于列表中的每一个词语,查看是否在停用词表中出现,如果出现则将其删除。 In [4]
def preprocess(data): stopwords = open("data/data81223/stopwords.txt").read().split('\n') new_data = [] for row in data: row = jieba.lcut(row) # 中文分词 new_row = [] for word in row: if word not in stopwords: new_row.append(word) new_data.append(new_row) return np.array(new_data) corpus = preprocess(X)print(corpus[42])登录后复制
Building prefix dict from the default dictionary ... Dumping model to file cache /tmp/jieba.cache Loading model cost 0.835 seconds. Prefix dict has been built successfully.登录后复制
跌停, 虽未, 打开, 基金, 市价, 估值, 云天化, 登录后复制。
## 构建词表def build_vocab(data): word2id = {} vocab = Counter() for row in data: vocab.update(row) vocab = sorted(vocab.items(), key=lambda x: x[1], reverse=True) vocab = [('<PAD>', 0)] + list(vocab) + [('<UNK>', 0)] word2id = {word[0]: i for i, word in enumerate(vocab)} return word2iddef get_ids(data, word2id): ids = [] for row in data: id_ = list(map(lambda x: word2id.get(x, word2id['<UNK>']), row)) ids.append(id_) return np.array(ids)def padding(data): return Pad(pad_val=0)(data) word2id = build_vocab(corpus) ids = get_ids(corpus, word2id) padding_ids = padding(ids)print(padding_ids[42])print(padding_ids[56748])登录后复制
[ 2181 80653 4283 170 37 1070 1 19 12830 879 29555 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] [ 1908 4439 319 25054 126 2642 329 13 20 213 231 6516 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]登录后复制 In [6]
## 重写Datasetclass MyDataset(Dataset): def __init__(self, X, Y): super(MyDataset, self).__init__() self.X = X self.Y = Y def __len__(self): return self.X.shape[0] def __getitem__(self, index): return self.X[index], self.Y[index]登录后复制 In [7]
## 划分数据集训练集、测试集、开发集def split(X, Y): idx = [i for i in range(0, X.shape[0])] np.random.shuffle(idx) train_len = int(0.9 * len(idx)) return X[idx[:train_len]], Y[idx[:train_len]], \ X[idx[train_len:]], Y[idx[train_len:]] train_X, train_Y, test_X, test_Y = split(padding_ids, Y)print("Train Set: ", train_X.shape, train_Y.shape)print("Test Set: ", test_X.shape, test_Y.shape)登录后复制
Train Set: (677227, 75) (677227,) Test Set: (75248, 75) (75248,)登录后复制
(三)模型搭建
In [8]
## 搭建模型class TextCNN(paddle.nn.Layer): def __init__(self, vocab_size, embedding_size, classes, pretrained=None, kernel_num=100, kernel_size=[3, 4, 5], dropout=0.5): super(TextCNN, self).__init__() self.vocab_size = vocab_size self.embedding_size = embedding_size self.classes = classes self.pretrained = pretrained self.kernel_num = kernel_num self.kernel_size = kernel_size self.dropout = dropout if self.pretrained != None: self.embedding = nn.Embedding(num_embeddings=self.vocab_size, embedding_dim=self.embedding_size, padding_idx=0 ,_weight=pretrained) else: self.embedding = nn.Embedding(num_embeddings=self.vocab_size, embedding_dim=self.embedding_size, padding_idx=0) self.convs = nn.LayerList([nn.Conv2D(1, self.kernel_num, (kernel_size_, embedding_size)) for kernel_size_ in self.kernel_size]) self.dropout = nn.Dropout(self.dropout) self.linear = nn.Linear(3 * self.kernel_num, self.classes) def forward(self, x): embedding = self.embedding(x).unsqueeze(1) convs = [nn.ReLU()(conv(embedding)).squeeze(3) for conv in self.convs] pool_out = [nn.MaxPool1D(block.shape[2])(block).squeeze(2) for block in convs] pool_out = paddle.concat(pool_out, 1) logits = self.linear(pool_out) return logits登录后复制 In [9]
## 训练配置BATCH_SIZE = 50EMBEDDING_SIZE = 150LEARNING_RATE = 0.00005EPOCHS = 5 # 5device = paddle.device.get_device() #注意配置环境必须是GPU,否则以下训练程序将无法运行print(device)登录后复制
gpu:0登录后复制 In [10]
## 模型训练与评估Train_Loader = DataLoader(MyDataset(paddle.to_tensor(train_X), paddle.to_tensor(train_Y)), batch_size=BATCH_SIZE, shuffle=True) Test_Loader = DataLoader(MyDataset(paddle.to_tensor(test_X), paddle.to_tensor(test_Y)), batch_size=BATCH_SIZE, shuffle=True) model = TextCNN(vocab_size=len(word2id), embedding_size=EMBEDDING_SIZE, classes=len(Y_dict))print(model) optimizer = optim.Adam(parameters=model.parameters(), learning_rate=LEARNING_RATE) criterion = nn.CrossEntropyLoss()for epoch in range(0, EPOCHS): Train_Loss, Test_Loss = [], [] Train_Acc, Test_Acc = [], [] model.train() for i, (x, y) in enumerate(Train_Loader): x = x.cuda() y = y.cuda() pred = model(x) loss = criterion(pred, y) Train_Loss.append(loss.item()) Train_Acc.append(paddle.metric.accuracy(pred, y).numpy()) loss.backward() optimizer.step() optimizer.clear_grad() model.eval() for i, (x, y) in enumerate(Test_Loader): x = x.cuda() y = y.cuda() pred = model(x) Test_Loss.append(criterion(pred, y).item()) Test_Acc.append(paddle.metric.accuracy(pred, y).numpy()) print( "Epoch: [{}/{}] TrainLoss/TestLoss: {:.4f}/{:.4f} TrainAcc/TestAcc: {:.4f}/{:.4f}".format( \ epoch + 1, EPOCHS, \ np.mean(Train_Loss), np.mean(Test_Loss), \ np.mean(Train_Acc), np.mean(Test_Acc) \ ) \ )登录后复制
W0221 10:12:08.730477 101 device_context.cc:447] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 10.1, Runtime API Version: 10.1 W0221 10:12:08.736320 101 device_context.cc:465] device: 0, cuDNN Version: 7.6.登录后复制
TextCNN( (embedding): Embedding(249925, 150, padding_idx=0, sparse=False) (convs): LayerList( (0): Conv2D(1, 100, kernel_size=[3, 150], data_format=NCHW) (1): Conv2D(1, 100, kernel_size=[4, 150], data_format=NCHW) (2): Conv2D(1, 100, kernel_size=[5, 150], data_format=NCHW) ) (dropout): Dropout(p=0.5, axis=None, mode=upscale_in_train) (linear): Linear(in_features=300, out_features=14, dtype=float32) ) Epoch: [1/5] TrainLoss/TestLoss: 0.6914/0.3004 TrainAcc/TestAcc: 0.8045/0.9109 Epoch: [2/5] TrainLoss/TestLoss: 0.2333/0.2359 TrainAcc/TestAcc: 0.9311/0.9292 Epoch: [3/5] TrainLoss/TestLoss: 0.1707/0.2199 TrainAcc/TestAcc: 0.9486/0.9337 Epoch: [4/5] TrainLoss/TestLoss: 0.1364/0.2162 TrainAcc/TestAcc: 0.9585/0.9347 Epoch: [5/5] TrainLoss/TestLoss: 0.1125/0.2177 TrainAcc/TestAcc: 0.9657/0.9356登录后复制 In [11]
## 保存模型paddle.save(model.state_dict(), "TextCNN.pdparams") paddle.save(optimizer.state_dict(), "Adam.pdparams")登录后复制
(四)预测数据与效果检测
In [12]
预测数据准备:pred_X = pred_data['Sentence'].values; pred_corpus = preprocess(pred_X); pred_ids = get_ids(pred_corpus, wordd); pred_padding_ids = padding(pred_ids) 预测过程:PaddleLoader = DataLoader(MyDataset(paddle.to_tensor(pred_padding_ids), paddle.to_tensor(pred_padding_ids)), batch_size=BATCH_SIZE, shuffle=False) 模型评估:model.eval 生成预测结果:for i, (x, y) in enumerate(Pred_Loader): x = x.cuda; pred = model(x); label = np.argmax(pred, axis=, pred_Y += list(label) 可视化文本预测结果:pred_Name = [Y_dict[name] for name in pred_Y]; MyPred = pd.DataFrame({'ClassNo': pred_Y, 'ClassName': pred_Name, 'Sentence': pred_X}) 保存预测结果到CSV文件:MyPred.to_csv('pred.csv', index=False)登录后复制
( ClassNo ClassName Sentence 教育 北京君太百货璀璨秋色,满。优惠力度大! 科技 专业级单反相机,佳能单机售价。 股票 星展银行起诉内地客户,银行强硬态度无可奈何,RMB升值梦想破灭。
(五)封装功能 自定义文本输入
In [16]
model_params = paddle.load('TextCNN.pdparams') model_infer = TextCNN(vocab_size=len(word2id), embedding_size=EMBEDDING_SIZE, classes=len(Y_dict)) model_infer.set_state_dict(model_params) sentence = input() infer_corpus = preprocess([sentence]) infer_ids = get_ids(infer_corpus, word2id) infer_padding_ids = padding(infer_ids) model_infer.eval() val = model_infer(paddle.to_tensor(infer_padding_ids)) label = np.argmax(val, axis=1)[0]print("Sentence: ", sentence, " label: ", Y_dict[label])登录后复制
Sentence: 再次点球失手!如何评价梅西现在的踢球水平? label: 体育登录后复制
以上就是【AI达人创造营第二期】一文读懂textCNN模型原理的详细内容,更多请关注其它相关文章!