源码
# coding: UTF-8
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as npclass Config(object):"""配置参数类,用于存储模型和训练的超参数"""def __init__(self, dataset, embedding):self.model_name = 'TextRNN' # 模型名称self.train_path = dataset + '/data/train.txt' # 训练集路径self.dev_path = dataset + '/data/dev.txt' # 验证集路径self.test_path = dataset + '/data/test.txt' # 测试集路径self.class_list = [x.strip() for x in open(dataset + '/data/class.txt').readlines()] # 类别列表self.vocab_path = dataset + '/data/vocab.pkl' # 词表路径self.save_path = dataset + '/saved_dict/' + self.model_name + '.ckpt' # 模型保存路径self.log_path = dataset + '/log/' + self.model_name # 日志保存路径# 加载预训练词向量(若提供)self.embedding_pretrained = torch.tensor(np.load(dataset + '/data/' + embedding)["embeddings"].astype('float32')) \if embedding != 'random' else Noneself.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 训练设备# 模型超参数self.dropout = 0.5 # 随机失活率self.require_improvement = 1000 # 若超过该batch数效果未提升,则提前终止训练self.num_classes = len(self.class_list) # 类别数self.n_vocab = 0 # 词表大小(运行时赋值)self.num_epochs = 10 # 训练轮次self.batch_size = 128 # 批次大小self.pad_size = 32 # 句子填充/截断长度self.learning_rate = 1e-3 # 学习率# 词向量维度(使用预训练时与预训练维度对齐,否则设为300)self.embed = self.embedding_pretrained.size(1) \if self.embedding_pretrained is not None else 300self.hidden_size = 128 # LSTM隐藏层维度self.num_layers = 2 # LSTM层数'''基于LSTM的文本分类模型'''
class Model(nn.Module):def __init__(self, config):super(Model, self).__init__()# 词嵌入层:加载预训练词向量或随机初始化if config.embedding_pretrained is not None:self.embedding = nn.Embedding.from_pretrained(config.embedding_pretrained, freeze=False)else:self.embedding = nn.Embedding(config.n_vocab, config.embed, padding_idx=config.n_vocab - 1)# 双向LSTM层self.lstm = nn.LSTM(config.embed, config.hidden_size, config.num_layers,bidirectional=True, batch_first=True, dropout=config.dropout)# 全连接分类层self.fc = nn.Linear(config.hidden_size * 2, config.num_classes) # 双向LSTM输出维度翻倍def forward(self, x):x, _ = x # 输入x为(padded_seq, seq_len),此处取padded_seqout = self.embedding(x) # [batch_size, seq_len, embed_dim]out, _ = self.lstm(out) # LSTM输出维度 [batch_size, seq_len, hidden_size*2]# 取最后一个时间步的输出作为句子表示out = self.fc(out[:, -1, :]) # [batch_size, num_classes]return out
数据集
上图是我们这次做的文本分类。一共十个话题领域,我们的目标是输入一句话,模型能够实现对话题领域的区分。
上图是我们使用的数据集。前面的汉字部分是模型学习的文本,后面接一个tab键是对该文本的分类。
配置类
配置的重点是模型的超参数,这里分析一下模型涉及的超参数。
Dropout随机失活率
self.dropout = 0.5
在LSTM层之间随机屏蔽部分神经元输出,强迫模型学习冗余特征表示。公式:hdrop=h⊙mhdrop=h⊙m,其中mm是伯努利分布的0-1掩码。
早停阈值
elf.require_improvement = 1000
早停阈值的思想是:连续N个batch在验证集无精度提升则终止训练。首次训练数据的时候可能摸不清楚情况,设置了较大的epoch值,浪费掉大量训练时间。假设batch_size=128,数据集1万样本 , 每个epoch大约有78个batch。1000个batch的耐心期大约是13个epoch。
序列填充长度
self.pad_size = 32
序列填充长度的作用是,将变长文本序列处理为固定长度,满足神经网络批量处理的要求 。如果文本长度小于32,则填充特定的字符。如果文本长度大于32,则进行截断,保留32个字符。
序列填充长度通常使用95分位方式获得,获取代码如下
import numpy as np
lengths = [len(text.split()) for text in train_texts]
pad_size = int(np.percentile(lengths, 95)) # 覆盖95%样本
词向量维度
self.embed = 300
词向量的维度决定了语义空间的自由度 。假设我们使用字分割,每个文字对应一个300维的向量,将向量输入到模型中完成训练。
可以得出,向量维数越多,可以包含的信息数量就越多。但是并不是维度越高越好,下面的表是高维和低维的对比。
因子 | 低维(d=50) | 高维(d=1024) |
---|---|---|
语义区分度 | 相似词易混淆 | 可学习细粒度差异 |
计算复杂度 | O(Vd) 内存占用低 | GPU显存需求高 |
训练数据需求 | 1M+ tokens即可 | 需100M+ tokens |
下游任务适配性 | 适合简单分类任务 | 适合语义匹配任务 |
由于我们的数据量较小,所以使用较低的词向量维度。另外,如果使用预训练模型,词向量维度的值需要和预训练模型的值相同。
LSTM隐藏层维度
self.hidden_size = 128
隐藏层维度先卖个关子,下一章LSTM模型解析的时候讲。
模型搭建
Input Text → Embedding Layer → Bidirectional LSTM → Last Timestep Output → FC Layer → Class Probabilities
文本是无法直接被计算机识别的,所以文本需要映射为稠密向量才能输入给模型。因此在输入模型前要加一步向量映射。
class Model(nn.Module):def __init__(self, config):super(Model, self).__init__()# 词嵌入层:加载预训练词向量或随机初始化if config.embedding_pretrained is not None:self.embedding = nn.Embedding.from_pretrained(config.embedding_pretrained, freeze=False)else:self.embedding = nn.Embedding(config.n_vocab, config.embed, padding_idx=config.n_vocab - 1)# 双向LSTM层self.lstm = nn.LSTM(config.embed, config.hidden_size, config.num_layers,bidirectional=True, batch_first=True, dropout=config.dropout)# 全连接分类层self.fc = nn.Linear(config.hidden_size * 2, config.num_classes) # 双向LSTM输出维度翻倍def forward(self, x):x, _ = x # 输入x为(padded_seq, seq_len),此处取padded_seqout = self.embedding(x) # [batch_size, seq_len, embed_dim]out, _ = self.lstm(out) # LSTM输出维度 [batch_size, seq_len, hidden_size*2]# 取最后一个时间步的输出作为句子表示out = self.fc(out[:, -1, :]) # [batch_size, num_classes]return out
词嵌入层
首先构建词嵌入层,将本地的预训练embedding加载到pytorch里面。
双向LSTM层
我们使用双向LSTM模型,即将文本从左到右训练一次,也从右到左(倒着来)训练一次。
参数名 | 作用说明 | 典型值 |
---|---|---|
input_size | 输入特征维度(等于嵌入维度) | 300 |
hidden_size | 隐藏层维度 | 128/256 |
num_layers | LSTM堆叠层数 | 2-4 |
bidirectional | 启用双向LSTM | True |
batch_first | 输入输出使用(batch, seq, *)格式 | True |
dropout | 层间dropout概率(仅当num_layers>1时生效) | 0.5 |
全连接分类层
self.fc = nn.Linear(config.hidden_size * 2, config.num_classes)
全连接的输入通道数是隐藏层维度的两倍,原因是我们的模型是双向的,双向的结果都需要输出给全连接层。
前向传播
def forward(self, x):x, _ = x # 解包(padded_seq, seq_len)out = self.embedding(x) # [batch, seq_len, embed_dim]out, _ = self.lstm(out) # [batch, seq_len, 2*hidden_size]out = self.fc(out[:, -1, :]) # 取最后时刻的输出return out
首先提取输入x的填充张量。可以看到张量里有4760这种值,这个值是我们在文字长度不够时的填充内容。
经过embedding映射后可以看到,张量out里的数据变成128*32*300的维度,300的维度就是词向量维度,可以看到data里的数据都由原来的整数映射成了向量。
经过lstm运算后,out张量数据变成了128*32*128的维度
最终经过全连接层,out张量变成了128*10维度的张量。128是batch_size,10个维度即代表该条数据在10个分类中的概率。