NLP经典代码复盘-TextCNN-介绍+逐行代码解析
Part1-相关介绍
TextCNN是一种用于文本分类的卷积神经网络模型,由Kim于2014年提出。下面从几个关键点来分析它的原理:
- 文本表示和输入层
 
- 首先将文本中的每个词转换为词向量,可以使用预训练的词向量如Word2Vec或GloVe
 - 假设句子最大长度为n,词向量维度为k,则输入矩阵维度为n×k
 - 比如句子"我喜欢机器学习",每个词映射到100维词向量,就形成了一个5×100的矩阵
 
- 卷积层设计
 
- 使用多个不同尺寸的卷积核进行特征提取
 - 常用的卷积核大小有2、3、4等,表示同时考虑2-gram、3-gram、4-gram特征
 - 卷积核在垂直方向的大小等于词向量维度k
 - 例如,使用大小为3×k的卷积核,就能捕获3个词的局部特征
 
- 特征提取过程
 
- 卷积核在输入矩阵上从上到下滑动,步长通常为1
 - 每次滑动都对当前窗口进行卷积运算,得到一个特征值
 - 对于一个卷积核,最终得到一个特征图(feature map)
 - 比如输入长度为7的句子,使用大小为3的卷积核,得到长度为5的特征图
 
- 池化层操作
 
- 通常采用最大池化(max-pooling)操作
 - 对每个特征图取最大值,得到一个标量值
 - 这样不同长度的句子经过池化后都能得到固定长度的表示
 - 同时保留了最显著的特征,提高模型的鲁棒性
 
- 多通道设计
 
- 使用多个不同大小的卷积核并行处理
 - 每种大小的卷积核又包含多个,用于提取不同角度的特征
 - 假设使用大小为2、3、4的卷积核各100个
 - 池化后得到300维的特征向量
 
- 分类层
 
- 将池化得到的特征向量输入全连接层
 - 使用softmax函数进行多分类
 - 可以添加dropout防止过拟合
 
TextCNN的优势在于:
- 能够捕获局部相关的词特征
 - 通过不同大小的卷积核捕获多粒度的特征
 - 计算并行度高,训练速度快
 - 模型结构简单,易于理解和实现
 
这些设计让TextCNN在文本分类任务上取得了不错的效果,特别适合短文本分类场景。
N-gram
补充一些卷积相关的知识概念:
通过具体示例解释卷积的感受野(Receptive Field)。
感受野指的是卷积神经网络中,输出特征图上的一个像素点,能够"看到"输入图像上的区域大小。
不同层卷积的感受野扩展机制如下:
- 第一层卷积
- 基础感受野
 - 仅覆盖局部连续词
 - 提取最基础的局部特征
 
 - 第二层卷积
- 感受野扩大到第一层的2-3倍
 - 可以捕捉更广泛的上下文
 - 开始理解词语间的组合关系
 
 - 第三层及以上卷积
- 感受野呈指数级增长
 - 可覆盖整个句子的大部分区域
 - 捕捉全局语义和复杂语义特征
 
 
关键特点:
- 层数越深,感受野越大
 - 感受野增长不是线性的
 - 每一层都在前一层基础上抽象和扩展特征
 
多层卷积的层级操作主要通过以下机制实现:
- 第一层卷积
- 直接作用于输入的词嵌入向量
 - 捕捉局部特征(如2-3个词的组合)
 
 - 后续卷积层
- 以前一层卷积的输出作为输入
 - 在前一层特征基础上进行更高层次的特征提取
 - 通过池化层压缩和突出重要特征
 
 
具体过程:
- 第一层:词向量 → 局部特征
 - 第二层:局部特征 → 更抽象的组合特征
 - 第三层:组合特征 → 全局语义特征
 
关键点:
- 每层卷积逐步抽象
 - 感受野持续扩大
 - 特征从局部到全局逐步构建
 
文本示例:"这部电影虽然特效很炫,但剧情平淡无奇"
第一层卷积(2-gram):
- 捕捉局部特征
 - 提取短语:
- "这部电影"
 - "虽然特效"
 - "特效很炫"
 - "剧情平淡"
 - "平淡无奇"
 
 
第二层卷积(3-gram):
- 基于第一层特征
 - 提取更复杂的组合:
- "这部电影虽然"
 - "特效很炫但"
 - "剧情平淡无奇"
 
 
第三层卷积(4-gram):
- 进一步抽象
 - 捕捉全局语义:
- "这部电影虽然特效"
 - "特效很炫但剧情平淡"
 
 
# 多个卷积层,不同大小的卷积核 self.conv1 = nn.Conv1d(300, 100, kernel_size=2) # 2-gram self.conv2 = nn.Conv1d(300, 100, kernel_size=3) # 3-gram self.conv3 = nn.Conv1d(300, 100, kernel_size=4) # 4-gram
第二层卷积如何叠加:
原始文本: "这部电影特效很炫,但剧情平淡无奇"
第一层卷积输出:
- 2-gram特征:
- "这部电影"
 - "特效很炫"
 - "剧情平淡"
 
 
第二层卷积操作:
- 在第一层特征基础上
 - 使用新的卷积核
 - 重新提取特征
 - 提取更抽象的组合特征:
- "这部电影特效"
 - "特效很炫但"
 - "剧情平淡无奇"
 
 
关键步骤:
- 输入第一层特征图
 - 应用新卷积核
 - 提取更高层特征
 
特征递进:
- 第一层:词级别特征
 - 第二层:短语级别特征
 
Part2-代码逐行解释
import numpy as np # 导入NumPy库,用于数值计算 import torch # 导入PyTorch库,用于构建和训练神经网络 import torch.nn as nn # 导入神经网络模块 import torch.optim as optim # 导入优化器模块 import torch.nn.functional as F # 导入神经网络函数模块,包含常用的激活函数等 class TextCNN(nn.Module): def __init__(self): super(TextCNN, self).__init__() # 初始化父类 self.num_filters_total = num_filters * len(filter_sizes) # 计算卷积层总滤波器数量 self.W = nn.Embedding(vocab_size, embedding_size) # 定义嵌入层,将词汇索引转换为向量表示,vocab_size是词汇表大小,embedding_size是嵌入维度 self.Weight = nn.Linear(self.num_filters_total, num_classes, bias=False) # 定义线性层,将卷积层的输出映射到类别空间,num_classes是类别数量 self.Bias = nn.Parameter(torch.ones([num_classes])) # 定义偏置参数,形状为[num_classes] # 定义多个卷积层,每个卷积层的滤波器大小不同 self.filter_list = nn.ModuleList([nn.Conv2d(1, num_filters, (size, embedding_size)) for size in filter_sizes]) def forward(self, X): embedded_chars = self.W(X) # 通过嵌入层将输入X转换为嵌入向量,形状为[batch_size, sequence_length, embedding_size] embedded_chars = embedded_chars.unsqueeze(1) # 在嵌入向量中添加通道维度,形状变为[batch_size, 1, sequence_length, embedding_size] pooled_outputs = [] # 初始化池化输出列表,用于存储不同滤波器大小的池化结果 for i, conv in enumerate(self.filter_list): # conv的形状为[输入通道(=1), 输出通道(=num_filters), (滤波器高度, 滤波器宽度), 偏置选项] h = F.relu(conv(embedded_chars)) # 对嵌入向量进行卷积操作,并应用ReLU激活函数,h的形状为[batch_size, num_filters, new_height, 1] # mp的形状为[(滤波器高度, 滤波器宽度)] mp = nn.MaxPool2d((sequence_length - filter_sizes[i] + 1, 1)) # 定义最大池化层,池化窗口大小为(sequence_length - filter_sizes[i] + 1, 1) # pooled的形状为[batch_size, num_filters, 1, 1] pooled = mp(h).permute(0, 3, 2, 1) # 对池化结果进行维度重排,形状变为[batch_size, 1, 1, num_filters] pooled_outputs.append(pooled) # 将池化结果添加到池化输出列表中 h_pool = torch.cat(pooled_outputs, len(filter_sizes)) # 将所有池化输出在最后一个维度上拼接,形状为[batch_size, 1, 1, num_filters_total] h_pool_flat = torch.reshape(h_pool, [-1, self.num_filters_total]) # 将拼接后的结果展平,形状变为[batch_size, num_filters_total] model = self.Weight(h_pool_flat) + self.Bias # 通过线性层映射到类别空间,并加上偏置,形状为[batch_size, num_classes] return model # 返回模型输出 if __name__ == '__main__': embedding_size = 2 # 定义嵌入维度为2 sequence_length = 3 # 定义序列长度为3,即每个句子包含3个词 num_classes = 2 # 定义类别数量为2 filter_sizes = [2, 2, 2] # 定义滤波器大小列表,这里每个滤波器的高度为2,宽度为embedding_size num_filters = 3 # 定义每个滤波器大小的滤波器数量为3 # 定义6个句子的列表,每个句子的长度为3 sentences = ["i love you", "he loves me", "she likes baseball", "i hate you", "sorry for that", "this is awful"] labels = [1, 1, 1, 0, 0, 0] # 定义标签,1表示正面情感,0表示负面情感 word_list = " ".join(sentences).split() # 将所有句子连接成一个字符串并分割成单词列表 word_list = list(set(word_list)) # 去除重复单词,得到词汇表 word_dict = {w: i for i, w in enumerate(word_list)} # 创建单词到索引的字典 vocab_size = len(word_dict) # 词汇表大小,即词汇表中的单词数量 model = TextCNN() # 实例化TextCNN模型 criterion = nn.CrossEntropyLoss() # 定义损失函数为交叉熵损失 optimizer = optim.Adam(model.parameters(), lr=0.001) # 定义优化器为Adam,学习率为0.001 inputs = torch.LongTensor([np.asarray([word_dict[n] for n in sen.split()]) for sen in sentences]) # 将句子转换为词汇索引,并转换为PyTorch的长整型张量 targets = torch.LongTensor([out for out in labels]) # 将标签转换为PyTorch的长整型张量 # 训练过程 for epoch in range(5000): optimizer.zero_grad() # 清零梯度 output = model(inputs) # 前向传播,得到输出 # 输出形状为[batch_size, num_classes],目标批次形状为[batch_size] (长整型张量,不是one-hot编码) loss = criterion(output, targets) # 计算损失 if (epoch + 1) % 1000 == 0: print('Epoch:', '%04d' % (epoch + 1), 'cost =', '{:.6f}'.format(loss)) # 每1000个epoch打印一次损失 loss.backward() # 反向传播,计算梯度 optimizer.step() # 更新模型参数 # 测试 test_text = 'sorry hate you' # 定义测试文本 tests = [np.asarray([word_dict[n] for n in test_text.split()])] # 将测试文本转换为词汇索引列表 test_batch = torch.LongTensor(tests) # 将词汇索引列表转换为PyTorch的长整型张量 # 预测 predict = model(test_batch).data.max(1, keepdim=True)[1] # 对测试批次进行预测,选择概率最高的类别 if predict[0][0] == 0: print(test_text, "is Bad Mean...") # 如果预测结果为0,打印负面情感 else: print(test_text, "is Good Mean!!") # 如果预测结果为1,打印正面情感

                
            
        
浙公网安备 33010602011771号