NLP经典代码复盘-TextCNN-介绍+逐行代码解析

Part1-相关介绍

TextCNN是一种用于文本分类的卷积神经网络模型,由Kim于2014年提出。下面从几个关键点来分析它的原理:

  1. 文本表示和输入层
  • 首先将文本中的每个词转换为词向量,可以使用预训练的词向量如Word2Vec或GloVe
  • 假设句子最大长度为n,词向量维度为k,则输入矩阵维度为n×k
  • 比如句子"我喜欢机器学习",每个词映射到100维词向量,就形成了一个5×100的矩阵
  1. 卷积层设计
  • 使用多个不同尺寸的卷积核进行特征提取
  • 常用的卷积核大小有2、3、4等,表示同时考虑2-gram、3-gram、4-gram特征
  • 卷积核在垂直方向的大小等于词向量维度k
  • 例如,使用大小为3×k的卷积核,就能捕获3个词的局部特征
  1. 特征提取过程
  • 卷积核在输入矩阵上从上到下滑动,步长通常为1
  • 每次滑动都对当前窗口进行卷积运算,得到一个特征值
  • 对于一个卷积核,最终得到一个特征图(feature map)
  • 比如输入长度为7的句子,使用大小为3的卷积核,得到长度为5的特征图
  1. 池化层操作
  • 通常采用最大池化(max-pooling)操作
  • 对每个特征图取最大值,得到一个标量值
  • 这样不同长度的句子经过池化后都能得到固定长度的表示
  • 同时保留了最显著的特征,提高模型的鲁棒性
  1. 多通道设计
  • 使用多个不同大小的卷积核并行处理
  • 每种大小的卷积核又包含多个,用于提取不同角度的特征
  • 假设使用大小为2、3、4的卷积核各100个
  • 池化后得到300维的特征向量
  1. 分类层
  • 将池化得到的特征向量输入全连接层
  • 使用softmax函数进行多分类
  • 可以添加dropout防止过拟合

TextCNN的优势在于:

  • 能够捕获局部相关的词特征
  • 通过不同大小的卷积核捕获多粒度的特征
  • 计算并行度高,训练速度快
  • 模型结构简单,易于理解和实现

这些设计让TextCNN在文本分类任务上取得了不错的效果,特别适合短文本分类场景。

N-gram

N-gram 是一种基于统计语言模型的基本概念,指的是从文本中提取的连续的 N 个词或字符的序列。其中 N 表示要提取的词或字符的数量。

让我用具体例子说明:

假设我们有一个句子:"我喜欢机器学习"

可以提取的 N-gram 有:

  1. 1-gram (unigram):
    • 单个词:"我", "喜欢", "机器", "学习"
  2. 2-gram (bigram):
    • 两个连续的词:"我喜欢", "喜欢机器", "机器学习"
  3. 3-gram (trigram):
    • 三个连续的词:"我喜欢机器", "喜欢机器学习"

N-gram 的主要用途:

  • 预测下一个可能出现的词
  • 计算文本相似度
  • 文本特征提取
  • 语言模型训练

以搜索引擎为例:当你输入"北京天"时,系统会根据 N-gram 模型预测你可能要搜索:

  • "北京天气"
  • "北京天安门"
  • "北京天坛"

这就是基于历史数据中这些词组出现的频率来预测的。

在 TextCNN 中,不同大小的卷积核就相当于在提取不同长度的 N-gram 特征:

  • 大小为 2 的卷积核提取 2-gram 特征
  • 大小为 3 的卷积核提取 3-gram 特征
  • 大小为 4 的卷积核提取 4-gram 特征

这让模型能同时考虑不同大小的文本片段,从而更好地理解文本的语义信息。

补充一些卷积相关的知识概念:

通过具体示例解释卷积的感受野(Receptive Field)。

感受野指的是卷积神经网络中,输出特征图上的一个像素点,能够"看到"输入图像上的区域大小

不同层卷积的感受野扩展机制如下:

  1. 第一层卷积
    • 基础感受野
    • 仅覆盖局部连续词
    • 提取最基础的局部特征
  2. 第二层卷积
    • 感受野扩大到第一层的2-3倍
    • 可以捕捉更广泛的上下文
    • 开始理解词语间的组合关系
  3. 第三层及以上卷积
    • 感受野呈指数级增长
    • 可覆盖整个句子的大部分区域
    • 捕捉全局语义和复杂语义特征

关键特点:

  • 层数越深,感受野越大
  • 感受野增长不是线性的
  • 每一层都在前一层基础上抽象和扩展特征

多层卷积的层级操作主要通过以下机制实现:

  1. 第一层卷积
    • 直接作用于输入的词嵌入向量
    • 捕捉局部特征(如2-3个词的组合)
  2. 后续卷积层
    • 以前一层卷积的输出作为输入
    • 在前一层特征基础上进行更高层次的特征提取
    • 通过池化层压缩和突出重要特征

具体过程:

  • 第一层:词向量 → 局部特征
  • 第二层:局部特征 → 更抽象的组合特征
  • 第三层:组合特征 → 全局语义特征

关键点:

  • 每层卷积逐步抽象
  • 感受野持续扩大
  • 特征从局部到全局逐步构建

文本示例:"这部电影虽然特效很炫,但剧情平淡无奇"

第一层卷积(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特征:
    • "这部电影"
    • "特效很炫"
    • "剧情平淡"

第二层卷积操作:

  • 在第一层特征基础上
  • 使用新的卷积核
  • 重新提取特征
  • 提取更抽象的组合特征:
    • "这部电影特效"
    • "特效很炫但"
    • "剧情平淡无奇"

关键步骤:

  1. 输入第一层特征图
  2. 应用新卷积核
  3. 提取更高层特征

特征递进:

  • 第一层:词级别特征
  • 第二层:短语级别特征

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,打印正面情感

 Part3-学习链接

🔗相关链接1

🔗相关链接2

posted @ 2025-01-21 18:40  谁的青春不迷糊  阅读(304)  评论(0)    收藏  举报