当前位置:   article > 正文

【Pytorch神经网络实战案例】31 TextCNN模型分析IMDB数据集评论的积极与消极

【Pytorch神经网络实战案例】31 TextCNN模型分析IMDB数据集评论的积极与消极

卷积神经网络不仅在图像视觉领域有很好的效果,而且在基于文本的NLP领域也有很好的效果。TextCN如模型是卷积神经网络用于文本处理方面的一个模型。

在TextCNN模型中,通过多分支卷积技术实现对文本的分类功能。

1 TextCNN

1.1 TextCNN模型结构

TexCNN模型是利用卷积神经网络对文本进行分类的模型,该模型的结构可以分为以下4个层次:

1.1.1 词嵌入层

将每个词对应的向量转化成多维度的词嵌入向量,将每个句子当作一幅图来进行处理(词的个数词×嵌入向量维度)。

1.1.2 多分支卷积层

使用3、4、5等不同大小的卷积核对词嵌入转化后的句子做卷积操作,生成大小不同的特征数据。

1.1.3 多分支全局最大池化层

对多分支卷积层中输出的每个分支的特征数据做全局最大池化操作。

1.1.4 全连接分类输出层

将池化后的结果输入全连接网络中,输出分类个数,得到最终结果。

1.2 TextCNN模型图解

因为卷积神经网络具有提取局部特征的功能,所以可用卷积神经网络提取句子中类似N-Gram算法的关键信息。

1.3 数据集IMDB

MDB数据集相当于图片处理领域的MNIST数据集,在NLP任务中经常被使用。

1.3.1 IMDB结构组成

IMDB数据集包含50000条评论,平均分成训练数据集(25000条评论)和测试数据集(25000条评论)。标签的总体分布是平衡的(25000条正面评论和25000条负面评论)。

另外,还包括额外的50000份无标签文件,用于无监督学习。

1.3.2 IMDB文件夹组成

IMDB数据集主要包括两个文件夹train与test,分别存放训练数据集与测试数据集。每个文件夹中都包含正样本和负样本,分别放在pos与neg子文件中。rain文件夹下还额外包含一个unsup子文件夹,用于非监督训练。

1.3.3 IMDB文件命名规则

每个样本文件的命名规则为“序号_评级”。其中“评级”可以分为0~9级。

 IMDB是torchtext库的内置数据集,可以直接通过运行torchtext库的接口进行获取。

2 代码实现:TextCNN模型分析IMDB数据集评论的积极与消极

2.1 案例描述

同一个记录评论语句的数据集,分为正面和负面两种情绪。通过训练,让模型能够理解正面与负面两种情绪的语义,并对评论文本进行分类。

2.1.1 案例理解分析

本例的任务可以理解为通过句子中的关键信息进行语义分类,这与TextCNN模型的功能是相匹配的。TextCNN模型中使用了池化操作,在这个过程中丢失了一些信息,所以导致该模型所表征的句子特征有限。如果要使用处理相近语义的分类任务,则还需要对其进一步进行调整。

2.2 代码实现:引入基础库: 固定PyTorch中的随机种子和GPU运算方式---TextCNN.py(第1部分)

  1. # 1.1 引入基础库: 固定PyTorch中的随机种子和GPU运算方式。
  2. import random #引入基础库
  3. import time
  4. import torch#引入PyTorch库
  5. import torch.nn as nn
  6. import torch.nn.functional as F
  7. from torchtext.legacy import data ,datasets,vocab #引入文本处理库
  8. import spacy
  9. torch.manual_seed(1234) # 固定随机种子,使其每次运行时对权重参数的初始化值一致。
  10. # 固定GPU运算方式:提高GPU的运算效率,通常PyTorch会调用自动寻找最适合当前配置的高效算法进行计算,这一过程会导致每次运算的结果可能出现不一致的情况。
  11. torch.backends.cudnn.deterministic = True # 表明不使用寻找高效算法的功能,使得每次的运算结果一致。[仅GPU有效]

2.3 代码实现:用torchtext加载IMDB并拆分为数据集---TextCNN.py(第2部分)

  1. # 1.2 用torchtext加载IMDB并拆分为数据集
  2. # IMDB是torchtext库的内置数据集,可以直接通过torchtext库中的datasets.MDB进行处理。
  3. # 在处理之前将数据集的字段类型和分词方法指定正确即可。
  4. # 定义字段,并按照指定标记化函数进行分词
  5. TEXT = data.Field(tokenize = 'spacy',lower=True) # data.Field函数指定数据集中的文本字段用spaCy库进行分词处理,并将其统一改为小写字母。tokenize参数,不设置则默认使用str。
  6. LABEL = data.LabelField(dtype=torch.float)
  7. # 加载数据集,并根据IMDB两个文件夹,返回两个数据集。
  8. # datasets.MDB.splits()进行数据集的加载。该代码执行时会在本地目录的.data文件夹下查找是否有MDB数据集,如果没有,则下载;如果有,则将其加载到内存。
  9. # 被载入内存的数据集会放到数据集对象train_data与test_data中。
  10. train_data , test_data = datasets.IMDB.splits(text_field=TEXT,label_field=LABEL)
  11. print("-----------输出一条数据-----------")
  12. # print(vars(train_data.example[0]),len(train_data.example))
  13. print(vars(train_data.examples[0]),len(train_data.examples))
  14. print("---------------------------")
  15. # 将训练数据集再次拆分
  16. # 从训练数据中拆分出一部分作为验证数据集。数据集对象train_data的split方法默认按照70%、30%的比例进行拆分。
  17. train_data,valid_data = train_data.split(random_state = random.seed(1234))
  18. print("训练数据集: ", len(train_data),"条")
  19. print("验证数据集: ", len(valid_data),"条")
  20. print("测试数据集: ", len(test_data),"条")

2.4 代码实现:加载预训练词向量并进行样本数据化---TextCNN.py(第3部分)

  1. # 1.3 加载预训练词向量并进行样本数据化
  2. # 将数据集中的样本数据转化为词向量,并将其按照指定的批次大小进行组合。
  3. # buld_vocab方法实现文本到词向量数据的转化:从数据集对象train_data中取出前25000个高频词,并用指定的预训练词向量glove.6B.100d进行映射。
  4. TEXT.build_vocab(train_data,max_size=25000,vectors="glove.6B.100d",unk_init = torch.Tensor.normal_) # 将样本数据转化为词向量
  5. # glove.6B.100d为torchtext库中内置的英文词向量,主要将每个词映射成维度为100的浮点型数据,该文件会被下载到本地.vector_cache文件夹下。
  6. LABEL.build_vocab(train_data)
  7. # ---start---创建批次数据:将数据集按照指定批次进行组合。
  8. BATCH_SIZE = 64
  9. device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
  10. train_iterator, valid_iterator, test_iterator = data.BucketIterator.splits((train_data, valid_data, test_data), batch_size = BATCH_SIZE, device = device)
  11. # ---end---创建批次数据:将数据集按照指定批次进行组合。

2.5 代码实现:定义带有Mish激活函数的TextCNN模型---TextCNN.py(第4部分)

  1. class Mish(nn.Module):
  2. def __init__(self):
  3. super(Mish, self).__init__()
  4. def forward(self,x):
  5. x = x * (torch.tanh(F.softplus(x)))
  6. return x
  7. # 在TextCNN类中,一共有两个方法:
  8. # ①初始化方法.按照指定个数定义多分支卷积层,并将它们统一放在nn.ModuleList数组中。
  9. # ②前向传播方法:先将输入数据依次输入每个分支的卷积层中进行处理,再对处理结果进行最大池化,最后对池化结果进行连接并回归处理
  10. class TextCNN(nn.Module): #定义TextCNN模型
  11. # TextCNN类继承了nn.Module类,在该类中定义的网络层列表必须要使用nn.ModuleList进行转化,才可以被TextCNN类识别。
  12. # 如果直接使用列表的话,在训练模型时无法通过TextCNN类对象的parameters方法获得权重。
  13. # 定义初始化方法
  14. def __init__(self, vocab_size, embedding_dim, n_filters, filter_sizes, output_dim,dropout, pad_idx):
  15. super().__init__()
  16. self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx = pad_idx) # 定义词向量权重
  17. # 定义多分支卷积层
  18. # 将定义好的多分支卷积层以列表形式存放,以便在前向传播方法中使用。
  19. # 每个分支中卷积核的第一个维度由参数filter_sizes设置,第二个维度都是embedding_dim,即只在纵轴的方向上实现了真正的卷积操作,在横轴的方向上是全尺度卷积,可以起到一维卷积的效果。
  20. self.convs = nn.ModuleList([nn.Conv2d(in_channels = 1,out_channels = n_filters,kernel_size = (fs, embedding_dim))
  21. for fs in filter_sizes]) #########注意不能用list
  22. # 定义输出层
  23. self.fc = nn.Linear(len(filter_sizes) * n_filters, output_dim)
  24. self.dropout = nn.Dropout(dropout)
  25. self.mish = Mish() # 实例化激活函数对象
  26. # 定义前向传播方法
  27. def forward(self,text): # 输入形状为[sent len,batch size]
  28. text = text.permute(1, 0) # 将形状变为[batch size, sent len]
  29. embedded = self.embedding(text) # 对于输入数据进行词向量映射,形状为[batch size, sent len, emb dim]
  30. embedded = embedded.unsqueeze(1) # 进行维度变化,形状为[batch size, 1, sent len, emb dim]
  31. # len(filter_sizes)个元素,每个元素形状为[batch size, n_filters, sent len - filter_sizes[n] + 1]
  32. # 多分支卷积处理
  33. conved = [self.mish(conv(embedded)).squeeze(3) for conv in self.convs] # 将输入数据进行多分支卷积处理。该代码执行后,会得到一个含有len(fiter_sizes)个元素的列表,其中每个元素形状为[batchsize,n_filters,sentlen-fltersizes[n]+1],该元素最后一个维度的公式是由卷积公式计算而来的。
  34. # 对于每个卷积结果进行最大池化操作
  35. pooled = [F.max_pool1d(conv, conv.shape[2]).squeeze(2) for conv in conved]
  36. # 将池化结果进行连接
  37. cat = self.dropout(torch.cat(pooled, dim=1)) # 形状为[batch size, n_filters * len(filter_sizes)]
  38. return self.fc(cat) # 输入全连接,进行回归输出

2.6 代码实现:用数据集参数实例化模型---TextCNN.py(第5部分)

  1. # 1.5 用数据集参数实例化模型
  2. if __name__ == '__main__':
  3. # 根据处理好的数据集参数对TextCNN模型进行实例化。
  4. INPUT_DIM = len(TEXT.vocab) # 25002
  5. EMBEDDING_DIM = TEXT.vocab.vectors.size()[1] # 100
  6. N_FILTERS = 100 # 定义每个分支的数据通道数量
  7. FILTER_SIZES = [3, 4, 5] # 定义多分支卷积中每个分支的卷积核尺寸
  8. OUTPUT_DIM = 1 # 定义输出维度
  9. DROPOUT = 0.5 # 定义Dropout丢弃率
  10. PAD_IDX = TEXT.vocab.stoi[TEXT.pad_token] # 定义填充值:获取数据集中填充字符对应的索引。在词向量映射过程中对齐数据时会使用该索引进行填充。
  11. # 实例化模型
  12. model = TextCNN(INPUT_DIM, EMBEDDING_DIM, N_FILTERS, FILTER_SIZES, OUTPUT_DIM, DROPOUT, PAD_IDX)

2.7 代码实现:用预训练词向量初始化模型---TextCNN.py(第6部分)

  1. # 1.6 用预训练词向量初始化模型:将加载好的TEXT字段词向量复制到模型中,为其初始化。
  2. # 复制词向量
  3. model.embedding.weight.data.copy_(TEXT.vocab.vectors)
  4. # 将填充的词向量清0
  5. UNK_IDX = TEXT.vocab.stoi[TEXT.unk_token]
  6. model.embedding.weight.data[UNK_IDX] = torch.zeros(EMBEDDING_DIM) #对未识别词进行清零处理 :使该词在词向量空间中失去意义,目的是防止后面填充字符对原有的词向量空间进行干扰。
  7. model.embedding.weight.data[PAD_IDX] = torch.zeros(EMBEDDING_DIM) #对填充词进行清零处理 :使该词在词向量空间中失去意义,目的是防止后面填充字符对原有的词向量空间进行干扰。

2.8 代码实现:使用Ranger优化器训练模型---TextCNN.py(第7部分)

  1. # 1.7 使用Ranger优化器训练模型
  2. import torch.optim as optim # 引入优化器库
  3. from functools import partial # 引入偏函数库
  4. from ranger import * # 载入Ranger优化器
  5. # 为Ranger优化器设置参数
  6. opt_func = partial(Ranger, betas=(.9, 0.99), eps=1e-6) # betas=(Momentum,alpha)
  7. optimizer = opt_func(model.parameters(), lr=0.004)
  8. # 定义损失函数
  9. criterion = nn.BCEWithLogitsLoss() # nn.BCEWithLogitsLoss函数是带有Sigmoid函数的二分类交叉熵,即先对模型的输出结果进行Sigmoid计算,再对其余标签一起做Cross_entropy计算。
  10. # 分配运算资源
  11. model = model.to(device)
  12. criterion = criterion.to(device)
  13. # 定义函数,计算精确率
  14. def binary_accuracy(preds, y): # 计算准确率
  15. rounded_preds = torch.round(torch.sigmoid(preds)) # 把概率的结果 四舍五入
  16. correct = (rounded_preds == y).float() # True False -> 转为 1, 0
  17. acc = correct.sum() / len(correct)
  18. return acc # 返回精确率
  19. #定义函数,训练模型
  20. def train(model, iterator, optimizer, criterion):
  21. epoch_loss = 0
  22. epoch_acc = 0
  23. model.train() # 设置模型标志,保证Dropout在训练模式下
  24. for batch in iterator: # 遍历数据集进行训练
  25. optimizer.zero_grad()
  26. predictions = model(batch.text).squeeze(1) # 在第1个维度上去除维度
  27. loss = criterion(predictions, batch.label) # 计算损失
  28. acc = binary_accuracy(predictions, batch.label) # 计算精确率
  29. loss.backward() # 损失函数反向
  30. optimizer.step() # 优化处理
  31. epoch_loss += loss.item() # 统计损失
  32. epoch_acc += acc.item() # 统计精确率
  33. return epoch_loss / len(iterator), epoch_acc / len(iterator)
  34. # 定义函数,评估模型
  35. def evaluate(model, iterator, criterion):
  36. epoch_loss = 0
  37. epoch_acc = 0
  38. model.eval() # 设置模型标志,保证Dropout在评估模型下
  39. with torch.no_grad(): # 禁止梯度计算
  40. for batch in iterator:
  41. predictions = model(batch.text).squeeze(1) # 计算结果
  42. loss = criterion(predictions, batch.label) # 计算损失
  43. acc = binary_accuracy(predictions, batch.label) # 计算精确率
  44. epoch_loss += loss.item()
  45. epoch_acc += acc.item()
  46. return epoch_loss / len(iterator), epoch_acc / len(iterator)
  47. # 定义函数,计算时间差
  48. def epoch_time(start_time, end_time):
  49. elapsed_time = end_time - start_time
  50. elapsed_mins = int(elapsed_time / 60)
  51. elapsed_secs = int(elapsed_time - (elapsed_mins * 60))
  52. return elapsed_mins, elapsed_secs
  53. N_EPOCHS = 100 # 设置训练的迭代次数
  54. best_valid_loss = float('inf') # 设置损失初始值,用于保存最优模型
  55. for epoch in range(N_EPOCHS): # 按照迭代次数进行训练
  56. start_time = time.time()
  57. train_loss, train_acc = train(model, train_iterator, optimizer, criterion)
  58. valid_loss, valid_acc = evaluate(model, valid_iterator, criterion)
  59. end_time = time.time()
  60. # 计算迭代时间消耗
  61. epoch_mins, epoch_secs = epoch_time(start_time, end_time)
  62. if valid_loss < best_valid_loss: # 保存最优模型
  63. best_valid_loss = valid_loss
  64. torch.save(model.state_dict(), 'textcnn-model.pt')
  65. # 输出训练结果
  66. print(f'Epoch: {epoch + 1:02} | Epoch Time: {epoch_mins}m {epoch_secs}s')
  67. print(f'\t训练损失: {train_loss:.3f} | 训练精确率: {train_acc * 100:.2f}%')
  68. print(f'\t Val. Loss: {valid_loss:.3f} | Val. Acc: {valid_acc * 100:.2f}%')
  69. # 测试模型效果
  70. model.load_state_dict(torch.load('textcnn-model.pt'))
  71. test_loss, test_acc = evaluate(model, test_iterator, criterion)
  72. print(f'Test Loss: {test_loss:.3f} | Test Acc: {test_acc * 100:.2f}%')

2.9 代码实现:使用模型进行训练---TextCNN.py(第8部分)

  1. # 1.8 使用模型进行训练:编写模型预测接口函数,对指定句子进行预测。列举几个句子输入模型预测接口函数进行预测,查看预测结果。
  2. nlp = spacy.load("en_core_web_sm")# 用spacy加载英文语言包
  3. # 定义函数,实现预测接口
  4. # (1)将长度不足5的句子用′<pad>'字符补齐。(2)将句子中的单词转为索引。
  5. # (3)为张量增加维度,以与训练场景下的输入形状保持一致。(4)输入模型进行预测,并对结果进行Sigmoid计算。因为模型在训练时,使用的计算损失函数自带Sigmoid处理,但模型中没有Sigmoid处理,所以要对结果增加Sigmoid处理。
  6. def predict_sentiment(model, sentence, min_len=5): # 设置最小长度为5
  7. model.eval() # 设置模型标志,保证Dropout在评估模型下
  8. tokenized = nlp.tokenizer(sentence).text.split() #拆分输入的句子
  9. if len(tokenized) < min_len: # 长度不足,在后面填充
  10. tokenized += ['<pad>'] * (min_len - len(tokenized))
  11. indexed = [TEXT.vocab.stoi[t] for t in tokenized] # 将单词转化为索引
  12. tensor = torch.LongTensor(indexed).to(device)
  13. tensor = tensor.unsqueeze(1) # 为张量增加维度,模拟批次
  14. prediction = torch.sigmoid(model(tensor)) # 输入模型进行预测
  15. return prediction.item() # 返回预测结果
  16. # 使用句子进行预测:大于0.5为正面评论,小于0.5为负面评论
  17. sen = "This film is terrible"
  18. print('\n预测 sen = ', sen)
  19. print('预测 结果:', predict_sentiment(model, sen))
  20. sen = "This film is great"
  21. print('\n预测 sen = ', sen)
  22. print('预测 结果:', predict_sentiment(model, sen))
  23. sen = "I like this film very much!"
  24. print('\n预测 sen = ', sen)
  25. print('预测 结果:', predict_sentiment(model, sen))

3 代码总览

3.1 TextCNN.py

  1. # 1.1 引入基础库: 固定PyTorch中的随机种子和GPU运算方式。
  2. import random #引入基础库
  3. import time
  4. import torch#引入PyTorch库
  5. import torch.nn as nn
  6. import torch.nn.functional as F
  7. from torchtext.legacy import data ,datasets,vocab #引入文本处理库
  8. import spacy
  9. torch.manual_seed(1234) # 固定随机种子,使其每次运行时对权重参数的初始化值一致。
  10. # 固定GPU运算方式:提高GPU的运算效率,通常PyTorch会调用自动寻找最适合当前配置的高效算法进行计算,这一过程会导致每次运算的结果可能出现不一致的情况。
  11. torch.backends.cudnn.deterministic = True # 表明不使用寻找高效算法的功能,使得每次的运算结果一致。[仅GPU有效]
  12. # 1.2 用torchtext加载IMDB并拆分为数据集
  13. # IMDB是torchtext库的内置数据集,可以直接通过torchtext库中的datasets.MDB进行处理。
  14. # 在处理之前将数据集的字段类型和分词方法指定正确即可。
  15. # 定义字段,并按照指定标记化函数进行分词
  16. TEXT = data.Field(tokenize = 'spacy',lower=True) # data.Field函数指定数据集中的文本字段用spaCy库进行分词处理,并将其统一改为小写字母。tokenize参数,不设置则默认使用str。
  17. LABEL = data.LabelField(dtype=torch.float)
  18. # 加载数据集,并根据IMDB两个文件夹,返回两个数据集。
  19. # datasets.MDB.splits()进行数据集的加载。该代码执行时会在本地目录的.data文件夹下查找是否有MDB数据集,如果没有,则下载;如果有,则将其加载到内存。
  20. # 被载入内存的数据集会放到数据集对象train_data与test_data中。
  21. train_data , test_data = datasets.IMDB.splits(text_field=TEXT,label_field=LABEL)
  22. print("-----------输出一条数据-----------")
  23. # print(vars(train_data.example[0]),len(train_data.example))
  24. print(vars(train_data.examples[0]),len(train_data.examples))
  25. print("---------------------------")
  26. # 将训练数据集再次拆分
  27. # 从训练数据中拆分出一部分作为验证数据集。数据集对象train_data的split方法默认按照70%、30%的比例进行拆分。
  28. train_data,valid_data = train_data.split(random_state = random.seed(1234))
  29. print("训练数据集: ", len(train_data),"条")
  30. print("验证数据集: ", len(valid_data),"条")
  31. print("测试数据集: ", len(test_data),"条")
  32. # 1.3 加载预训练词向量并进行样本数据化
  33. # 将数据集中的样本数据转化为词向量,并将其按照指定的批次大小进行组合。
  34. # buld_vocab方法实现文本到词向量数据的转化:从数据集对象train_data中取出前25000个高频词,并用指定的预训练词向量glove.6B.100d进行映射。
  35. TEXT.build_vocab(train_data,max_size=25000,vectors="glove.6B.100d",unk_init = torch.Tensor.normal_) # 将样本数据转化为词向量
  36. # glove.6B.100d为torchtext库中内置的英文词向量,主要将每个词映射成维度为100的浮点型数据,该文件会被下载到本地.vector_cache文件夹下。
  37. LABEL.build_vocab(train_data)
  38. # ---start---创建批次数据:将数据集按照指定批次进行组合。
  39. BATCH_SIZE = 64
  40. device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
  41. train_iterator, valid_iterator, test_iterator = data.BucketIterator.splits((train_data, valid_data, test_data), batch_size = BATCH_SIZE, device = device)
  42. # ---end---创建批次数据:将数据集按照指定批次进行组合。
  43. # 1.4 定义带有Mish激活函数的TextCNN模型
  44. class Mish(nn.Module):
  45. def __init__(self):
  46. super(Mish, self).__init__()
  47. def forward(self,x):
  48. x = x * (torch.tanh(F.softplus(x)))
  49. return x
  50. # 在TextCNN类中,一共有两个方法:
  51. # ①初始化方法.按照指定个数定义多分支卷积层,并将它们统一放在nn.ModuleList数组中。
  52. # ②前向传播方法:先将输入数据依次输入每个分支的卷积层中进行处理,再对处理结果进行最大池化,最后对池化结果进行连接并回归处理
  53. class TextCNN(nn.Module): #定义TextCNN模型
  54. # TextCNN类继承了nn.Module类,在该类中定义的网络层列表必须要使用nn.ModuleList进行转化,才可以被TextCNN类识别。
  55. # 如果直接使用列表的话,在训练模型时无法通过TextCNN类对象的parameters方法获得权重。
  56. # 定义初始化方法
  57. def __init__(self, vocab_size, embedding_dim, n_filters, filter_sizes, output_dim,dropout, pad_idx):
  58. super().__init__()
  59. self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx = pad_idx) # 定义词向量权重
  60. # 定义多分支卷积层
  61. # 将定义好的多分支卷积层以列表形式存放,以便在前向传播方法中使用。
  62. # 每个分支中卷积核的第一个维度由参数filter_sizes设置,第二个维度都是embedding_dim,即只在纵轴的方向上实现了真正的卷积操作,在横轴的方向上是全尺度卷积,可以起到一维卷积的效果。
  63. self.convs = nn.ModuleList([nn.Conv2d(in_channels = 1,out_channels = n_filters,kernel_size = (fs, embedding_dim))
  64. for fs in filter_sizes]) #########注意不能用list
  65. # 定义输出层
  66. self.fc = nn.Linear(len(filter_sizes) * n_filters, output_dim)
  67. self.dropout = nn.Dropout(dropout)
  68. self.mish = Mish() # 实例化激活函数对象
  69. # 定义前向传播方法
  70. def forward(self,text): # 输入形状为[sent len,batch size]
  71. text = text.permute(1, 0) # 将形状变为[batch size, sent len]
  72. embedded = self.embedding(text) # 对于输入数据进行词向量映射,形状为[batch size, sent len, emb dim]
  73. embedded = embedded.unsqueeze(1) # 进行维度变化,形状为[batch size, 1, sent len, emb dim]
  74. # len(filter_sizes)个元素,每个元素形状为[batch size, n_filters, sent len - filter_sizes[n] + 1]
  75. # 多分支卷积处理
  76. conved = [self.mish(conv(embedded)).squeeze(3) for conv in self.convs] # 将输入数据进行多分支卷积处理。该代码执行后,会得到一个含有len(fiter_sizes)个元素的列表,其中每个元素形状为[batchsize,n_filters,sentlen-fltersizes[n]+1],该元素最后一个维度的公式是由卷积公式计算而来的。
  77. # 对于每个卷积结果进行最大池化操作
  78. pooled = [F.max_pool1d(conv, conv.shape[2]).squeeze(2) for conv in conved]
  79. # 将池化结果进行连接
  80. cat = self.dropout(torch.cat(pooled, dim=1)) # 形状为[batch size, n_filters * len(filter_sizes)]
  81. return self.fc(cat) # 输入全连接,进行回归输出
  82. # 1.5 用数据集参数实例化模型
  83. if __name__ == '__main__':
  84. # 根据处理好的数据集参数对TextCNN模型进行实例化。
  85. INPUT_DIM = len(TEXT.vocab) # 25002
  86. EMBEDDING_DIM = TEXT.vocab.vectors.size()[1] # 100
  87. N_FILTERS = 100 # 定义每个分支的数据通道数量
  88. FILTER_SIZES = [3, 4, 5] # 定义多分支卷积中每个分支的卷积核尺寸
  89. OUTPUT_DIM = 1 # 定义输出维度
  90. DROPOUT = 0.5 # 定义Dropout丢弃率
  91. PAD_IDX = TEXT.vocab.stoi[TEXT.pad_token] # 定义填充值:获取数据集中填充字符对应的索引。在词向量映射过程中对齐数据时会使用该索引进行填充。
  92. # 实例化模型
  93. model = TextCNN(INPUT_DIM, EMBEDDING_DIM, N_FILTERS, FILTER_SIZES, OUTPUT_DIM, DROPOUT, PAD_IDX)
  94. # 1.6 用预训练词向量初始化模型:将加载好的TEXT字段词向量复制到模型中,为其初始化。
  95. # 复制词向量
  96. model.embedding.weight.data.copy_(TEXT.vocab.vectors)
  97. # 将填充的词向量清0
  98. UNK_IDX = TEXT.vocab.stoi[TEXT.unk_token]
  99. model.embedding.weight.data[UNK_IDX] = torch.zeros(EMBEDDING_DIM) #对未识别词进行清零处理 :使该词在词向量空间中失去意义,目的是防止后面填充字符对原有的词向量空间进行干扰。
  100. model.embedding.weight.data[PAD_IDX] = torch.zeros(EMBEDDING_DIM) #对填充词进行清零处理 :使该词在词向量空间中失去意义,目的是防止后面填充字符对原有的词向量空间进行干扰。
  101. # 1.7 使用Ranger优化器训练模型
  102. import torch.optim as optim # 引入优化器库
  103. from functools import partial # 引入偏函数库
  104. from ranger import * # 载入Ranger优化器
  105. # 为Ranger优化器设置参数
  106. opt_func = partial(Ranger, betas=(.9, 0.99), eps=1e-6) # betas=(Momentum,alpha)
  107. optimizer = opt_func(model.parameters(), lr=0.004)
  108. # 定义损失函数
  109. criterion = nn.BCEWithLogitsLoss() # nn.BCEWithLogitsLoss函数是带有Sigmoid函数的二分类交叉熵,即先对模型的输出结果进行Sigmoid计算,再对其余标签一起做Cross_entropy计算。
  110. # 分配运算资源
  111. model = model.to(device)
  112. criterion = criterion.to(device)
  113. # 定义函数,计算精确率
  114. def binary_accuracy(preds, y): # 计算准确率
  115. rounded_preds = torch.round(torch.sigmoid(preds)) # 把概率的结果 四舍五入
  116. correct = (rounded_preds == y).float() # True False -> 转为 1, 0
  117. acc = correct.sum() / len(correct)
  118. return acc # 返回精确率
  119. #定义函数,训练模型
  120. def train(model, iterator, optimizer, criterion):
  121. epoch_loss = 0
  122. epoch_acc = 0
  123. model.train() # 设置模型标志,保证Dropout在训练模式下
  124. for batch in iterator: # 遍历数据集进行训练
  125. optimizer.zero_grad()
  126. predictions = model(batch.text).squeeze(1) # 在第1个维度上去除维度
  127. loss = criterion(predictions, batch.label) # 计算损失
  128. acc = binary_accuracy(predictions, batch.label) # 计算精确率
  129. loss.backward() # 损失函数反向
  130. optimizer.step() # 优化处理
  131. epoch_loss += loss.item() # 统计损失
  132. epoch_acc += acc.item() # 统计精确率
  133. return epoch_loss / len(iterator), epoch_acc / len(iterator)
  134. # 定义函数,评估模型
  135. def evaluate(model, iterator, criterion):
  136. epoch_loss = 0
  137. epoch_acc = 0
  138. model.eval() # 设置模型标志,保证Dropout在评估模型下
  139. with torch.no_grad(): # 禁止梯度计算
  140. for batch in iterator:
  141. predictions = model(batch.text).squeeze(1) # 计算结果
  142. loss = criterion(predictions, batch.label) # 计算损失
  143. acc = binary_accuracy(predictions, batch.label) # 计算精确率
  144. epoch_loss += loss.item()
  145. epoch_acc += acc.item()
  146. return epoch_loss / len(iterator), epoch_acc / len(iterator)
  147. # 定义函数,计算时间差
  148. def epoch_time(start_time, end_time):
  149. elapsed_time = end_time - start_time
  150. elapsed_mins = int(elapsed_time / 60)
  151. elapsed_secs = int(elapsed_time - (elapsed_mins * 60))
  152. return elapsed_mins, elapsed_secs
  153. N_EPOCHS = 100 # 设置训练的迭代次数
  154. best_valid_loss = float('inf') # 设置损失初始值,用于保存最优模型
  155. for epoch in range(N_EPOCHS): # 按照迭代次数进行训练
  156. start_time = time.time()
  157. train_loss, train_acc = train(model, train_iterator, optimizer, criterion)
  158. valid_loss, valid_acc = evaluate(model, valid_iterator, criterion)
  159. end_time = time.time()
  160. # 计算迭代时间消耗
  161. epoch_mins, epoch_secs = epoch_time(start_time, end_time)
  162. if valid_loss < best_valid_loss: # 保存最优模型
  163. best_valid_loss = valid_loss
  164. torch.save(model.state_dict(), 'textcnn-model.pt')
  165. # 输出训练结果
  166. print(f'Epoch: {epoch + 1:02} | Epoch Time: {epoch_mins}m {epoch_secs}s')
  167. print(f'\t训练损失: {train_loss:.3f} | 训练精确率: {train_acc * 100:.2f}%')
  168. print(f'\t Val. Loss: {valid_loss:.3f} | Val. Acc: {valid_acc * 100:.2f}%')
  169. # 测试模型效果
  170. model.load_state_dict(torch.load('textcnn-model.pt'))
  171. test_loss, test_acc = evaluate(model, test_iterator, criterion)
  172. print(f'Test Loss: {test_loss:.3f} | Test Acc: {test_acc * 100:.2f}%')
  173. # 1.8 使用模型进行训练:编写模型预测接口函数,对指定句子进行预测。列举几个句子输入模型预测接口函数进行预测,查看预测结果。
  174. nlp = spacy.load("en_core_web_sm")# 用spacy加载英文语言包
  175. # 定义函数,实现预测接口
  176. # (1)将长度不足5的句子用′<pad>'字符补齐。(2)将句子中的单词转为索引。
  177. # (3)为张量增加维度,以与训练场景下的输入形状保持一致。(4)输入模型进行预测,并对结果进行Sigmoid计算。因为模型在训练时,使用的计算损失函数自带Sigmoid处理,但模型中没有Sigmoid处理,所以要对结果增加Sigmoid处理。
  178. def predict_sentiment(model, sentence, min_len=5): # 设置最小长度为5
  179. model.eval() # 设置模型标志,保证Dropout在评估模型下
  180. tokenized = nlp.tokenizer(sentence).text.split() #拆分输入的句子
  181. if len(tokenized) < min_len: # 长度不足,在后面填充
  182. tokenized += ['<pad>'] * (min_len - len(tokenized))
  183. indexed = [TEXT.vocab.stoi[t] for t in tokenized] # 将单词转化为索引
  184. tensor = torch.LongTensor(indexed).to(device)
  185. tensor = tensor.unsqueeze(1) # 为张量增加维度,模拟批次
  186. prediction = torch.sigmoid(model(tensor)) # 输入模型进行预测
  187. return prediction.item() # 返回预测结果
  188. # 使用句子进行预测:大于0.5为正面评论,小于0.5为负面评论
  189. sen = "This film is terrible"
  190. print('\n预测 sen = ', sen)
  191. print('预测 结果:', predict_sentiment(model, sen))
  192. sen = "This film is great"
  193. print('\n预测 sen = ', sen)
  194. print('预测 结果:', predict_sentiment(model, sen))
  195. sen = "I like this film very much!"
  196. print('\n预测 sen = ', sen)
  197. print('预测 结果:', predict_sentiment(model, sen))

3.2 ranger.py

  1. #Ranger deep learning optimizer - RAdam + Lookahead combined.
  2. #https://github.com/lessw2020/Ranger-Deep-Learning-Optimizer
  3. #Ranger has now been used to capture 12 records on the FastAI leaderboard.
  4. #This version = 9.3.19
  5. #Credits:
  6. #RAdam --> https://github.com/LiyuanLucasLiu/RAdam
  7. #Lookahead --> rewritten by lessw2020, but big thanks to Github @LonePatient and @RWightman for ideas from their code.
  8. #Lookahead paper --> MZhang,G Hinton https://arxiv.org/abs/1907.08610
  9. #summary of changes:
  10. #full code integration with all updates at param level instead of group, moves slow weights into state dict (from generic weights),
  11. #supports group learning rates (thanks @SHolderbach), fixes sporadic load from saved model issues.
  12. #changes 8/31/19 - fix references to *self*.N_sma_threshold;
  13. #changed eps to 1e-5 as better default than 1e-8.
  14. import math
  15. import torch
  16. from torch.optim.optimizer import Optimizer, required
  17. import itertools as it
  18. class Ranger(Optimizer):
  19. def __init__(self, params, lr=1e-3, alpha=0.5, k=6, N_sma_threshhold=5, betas=(.95,0.999), eps=1e-5, weight_decay=0):
  20. #parameter checks
  21. if not 0.0 <= alpha <= 1.0:
  22. raise ValueError(f'Invalid slow update rate: {alpha}')
  23. if not 1 <= k:
  24. raise ValueError(f'Invalid lookahead steps: {k}')
  25. if not lr > 0:
  26. raise ValueError(f'Invalid Learning Rate: {lr}')
  27. if not eps > 0:
  28. raise ValueError(f'Invalid eps: {eps}')
  29. #parameter comments:
  30. # beta1 (momentum) of .95 seems to work better than .90...
  31. #N_sma_threshold of 5 seems better in testing than 4.
  32. #In both cases, worth testing on your dataset (.90 vs .95, 4 vs 5) to make sure which works best for you.
  33. #prep defaults and init torch.optim base
  34. defaults = dict(lr=lr, alpha=alpha, k=k, step_counter=0, betas=betas, N_sma_threshhold=N_sma_threshhold, eps=eps, weight_decay=weight_decay)
  35. super().__init__(params,defaults)
  36. #adjustable threshold
  37. self.N_sma_threshhold = N_sma_threshhold
  38. #now we can get to work...
  39. #removed as we now use step from RAdam...no need for duplicate step counting
  40. #for group in self.param_groups:
  41. # group["step_counter"] = 0
  42. #print("group step counter init")
  43. #look ahead params
  44. self.alpha = alpha
  45. self.k = k
  46. #radam buffer for state
  47. self.radam_buffer = [[None,None,None] for ind in range(10)]
  48. #self.first_run_check=0
  49. #lookahead weights
  50. #9/2/19 - lookahead param tensors have been moved to state storage.
  51. #This should resolve issues with load/save where weights were left in GPU memory from first load, slowing down future runs.
  52. #self.slow_weights = [[p.clone().detach() for p in group['params']]
  53. # for group in self.param_groups]
  54. #don't use grad for lookahead weights
  55. #for w in it.chain(*self.slow_weights):
  56. # w.requires_grad = False
  57. def __setstate__(self, state):
  58. print("set state called")
  59. super(Ranger, self).__setstate__(state)
  60. def step(self, closure=None):
  61. loss = None
  62. #note - below is commented out b/c I have other work that passes back the loss as a float, and thus not a callable closure.
  63. #Uncomment if you need to use the actual closure...
  64. #if closure is not None:
  65. #loss = closure()
  66. #Evaluate averages and grad, update param tensors
  67. for group in self.param_groups:
  68. for p in group['params']:
  69. if p.grad is None:
  70. continue
  71. grad = p.grad.data.float()
  72. if grad.is_sparse:
  73. raise RuntimeError('Ranger optimizer does not support sparse gradients')
  74. p_data_fp32 = p.data.float()
  75. state = self.state[p] #get state dict for this param
  76. if len(state) == 0: #if first time to run...init dictionary with our desired entries
  77. #if self.first_run_check==0:
  78. #self.first_run_check=1
  79. #print("Initializing slow buffer...should not see this at load from saved model!")
  80. state['step'] = 0
  81. state['exp_avg'] = torch.zeros_like(p_data_fp32)
  82. state['exp_avg_sq'] = torch.zeros_like(p_data_fp32)
  83. #look ahead weight storage now in state dict
  84. state['slow_buffer'] = torch.empty_like(p.data)
  85. state['slow_buffer'].copy_(p.data)
  86. else:
  87. state['exp_avg'] = state['exp_avg'].type_as(p_data_fp32)
  88. state['exp_avg_sq'] = state['exp_avg_sq'].type_as(p_data_fp32)
  89. #begin computations
  90. exp_avg, exp_avg_sq = state['exp_avg'], state['exp_avg_sq']
  91. beta1, beta2 = group['betas']
  92. #compute variance mov avg
  93. exp_avg_sq.mul_(beta2).addcmul_(1 - beta2, grad, grad)
  94. #compute mean moving avg
  95. exp_avg.mul_(beta1).add_(1 - beta1, grad)
  96. state['step'] += 1
  97. buffered = self.radam_buffer[int(state['step'] % 10)]
  98. if state['step'] == buffered[0]:
  99. N_sma, step_size = buffered[1], buffered[2]
  100. else:
  101. buffered[0] = state['step']
  102. beta2_t = beta2 ** state['step']
  103. N_sma_max = 2 / (1 - beta2) - 1
  104. N_sma = N_sma_max - 2 * state['step'] * beta2_t / (1 - beta2_t)
  105. buffered[1] = N_sma
  106. if N_sma > self.N_sma_threshhold:
  107. step_size = math.sqrt((1 - beta2_t) * (N_sma - 4) / (N_sma_max - 4) * (N_sma - 2) / N_sma * N_sma_max / (N_sma_max - 2)) / (1 - beta1 ** state['step'])
  108. else:
  109. step_size = 1.0 / (1 - beta1 ** state['step'])
  110. buffered[2] = step_size
  111. if group['weight_decay'] != 0:
  112. p_data_fp32.add_(-group['weight_decay'] * group['lr'], p_data_fp32)
  113. if N_sma > self.N_sma_threshhold:
  114. denom = exp_avg_sq.sqrt().add_(group['eps'])
  115. p_data_fp32.addcdiv_(-step_size * group['lr'], exp_avg, denom)
  116. else:
  117. p_data_fp32.add_(-step_size * group['lr'], exp_avg)
  118. p.data.copy_(p_data_fp32)
  119. #integrated look ahead...
  120. #we do it at the param level instead of group level
  121. if state['step'] % group['k'] == 0:
  122. slow_p = state['slow_buffer'] #get access to slow param tensor
  123. slow_p.add_(self.alpha, p.data - slow_p) #(fast weights - slow weights) * alpha
  124. p.data.copy_(slow_p) #copy interpolated weights to RAdam param tensor
  125. return loss

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/353338
推荐阅读
相关标签
  

闽ICP备14008679号