赞
踩
中文文本分类,TextCNN,TextRNN,FastText,TextRCNN,BiLSTM_Attention, DPCNN, Transformer, 基于pytorch
我从THUCNews中抽取了20万条新闻标题,文本长度在20到30之间。一共10个类别,每类2万条。
以字为单位输入模型,使用了预训练词向量:搜狗新闻 Word+Character 300d。
类别:财经、房产、股票、教育、科技、社会、时政、体育、游戏、娱乐。
数据集 | 数据量 |
---|---|
训练集 | 18万 |
验证集 | 1万 |
测试集 | 1万 |
python run.py --model TextCNN --word True
模型 | acc | 备注 |
---|---|---|
TextCNN | 91.22% | Kim 2014 经典的CNN文本分类 |
TextRNN | 91.12% | BiLSTM |
TextRNN_Att | 90.90% | BiLSTM+Attention |
TextRCNN | 91.54% | BiLSTM+池化 |
FastText | 92.23% | bow+bigram+trigram, 效果出奇的好 |
DPCNN | 91.25% | 深层金字塔CNN |
Transformer | 89.91% | 效果较差 |
bert | 94.83% | bert + fc |
ERNIE | 94.61% | 比bert略差(说好的中文碾压bert呢) |
- # 训练并测试:
- # TextCNN
- python run.py --model TextCNN
-
- # TextRNN
- python run.py --model TextRNN
-
- # TextRNN_Att
- python run.py --model TextRNN_Att
-
- # TextRCNN
- python run.py --model TextRCNN
-
- # FastText, embedding层是随机初始化的
- python run.py --model FastText --embedding random
-
- # DPCNN
- python run.py --model DPCNN
-
- # Transformer
- python run.py --model Transformer
模型都在models目录下,超参定义和模型定义在同一文件中。
models下文件
- # coding: UTF-8
- import torch
- import torch.nn as nn
- import torch.nn.functional as F
- import numpy as np
-
-
- class Config(object):
-
- """配置参数"""
- def __init__(self, dataset, embedding):
- self.model_name = 'DPCNN'
- 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', encoding='utf-8').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 None # 预训练词向量
- self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 设备
-
- self.dropout = 0.5 # 随机失活
- self.require_improvement = 1000 # 若超过1000batch效果还没提升,则提前结束训练
- self.num_classes = len(self.class_list) # 类别数
- self.n_vocab = 0 # 词表大小,在运行时赋值
- self.num_epochs = 20 # epoch数
- self.batch_size = 128 # mini-batch大小
- self.pad_size = 32 # 每句话处理成的长度(短填长切)
- self.learning_rate = 1e-3 # 学习率
- self.embed = self.embedding_pretrained.size(1)\
- if self.embedding_pretrained is not None else 300 # 字向量维度
- self.num_filters = 250 # 卷积核数量(channels数)
-
-
- '''Deep Pyramid Convolutional Neural Networks for Text Categorization'''
-
-
- 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)
- self.conv_region = nn.Conv2d(1, config.num_filters, (3, config.embed), stride=1)
- self.conv = nn.Conv2d(config.num_filters, config.num_filters, (3, 1), stride=1)
- self.max_pool = nn.MaxPool2d(kernel_size=(3, 1), stride=2)
- self.padding1 = nn.ZeroPad2d((0, 0, 1, 1)) # top bottom
- self.padding2 = nn.ZeroPad2d((0, 0, 0, 1)) # bottom
- self.relu = nn.ReLU()
- self.fc = nn.Linear(config.num_filters, config.num_classes)
-
- def forward(self, x):
- x = x[0]
- x = self.embedding(x)
- x = x.unsqueeze(1) # [batch_size, 250, seq_len, 1]
- x = self.conv_region(x) # [batch_size, 250, seq_len-3+1, 1]
-
- x = self.padding1(x) # [batch_size, 250, seq_len, 1]
- x = self.relu(x)
- x = self.conv(x) # [batch_size, 250, seq_len-3+1, 1]
- x = self.padding1(x) # [batch_size, 250, seq_len, 1]
- x = self.relu(x)
- x = self.conv(x) # [batch_size, 250, seq_len-3+1, 1]
- while x.size()[2] > 2:
- x = self._block(x)
- x = x.squeeze() # [batch_size, num_filters(250)]
- x = self.fc(x)
- return x
-
- def _block(self, x):
- x = self.padding2(x)
- px = self.max_pool(x)
-
- x = self.padding1(px)
- x = F.relu(x)
- x = self.conv(x)
-
- x = self.padding1(x)
- x = F.relu(x)
- x = self.conv(x)
-
- # Short Cut
- x = x + px
- return
- # coding: UTF-8
- import torch
- import torch.nn as nn
- import torch.nn.functional as F
- import numpy as np
-
-
- class Config(object):
-
- """配置参数"""
- def __init__(self, dataset, embedding):
- self.model_name = 'FastText'
- 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', encoding='utf-8').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 None # 预训练词向量
- self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 设备
-
- self.dropout = 0.5 # 随机失活
- self.require_improvement = 1000 # 若超过1000batch效果还没提升,则提前结束训练
- self.num_classes = len(self.class_list) # 类别数
- self.n_vocab = 0 # 词表大小,在运行时赋值
- self.num_epochs = 20 # epoch数
- self.batch_size = 128 # mini-batch大小
- self.pad_size = 32 # 每句话处理成的长度(短填长切)
- self.learning_rate = 1e-3 # 学习率
- self.embed = self.embedding_pretrained.size(1)\
- if self.embedding_pretrained is not None else 300 # 字向量维度
- self.hidden_size = 256 # 隐藏层大小
- self.n_gram_vocab = 250499 # ngram 词表大小
-
-
- '''Bag of Tricks for Efficient Text Classification'''
-
-
- 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)
- self.embedding_ngram2 = nn.Embedding(config.n_gram_vocab, config.embed)
- self.embedding_ngram3 = nn.Embedding(config.n_gram_vocab, config.embed)
- self.dropout = nn.Dropout(config.dropout)
- self.fc1 = nn.Linear(config.embed * 3, config.hidden_size)
- # self.dropout2 = nn.Dropout(config.dropout)
- self.fc2 = nn.Linear(config.hidden_size, config.num_classes)
-
- def forward(self, x):
-
- out_word = self.embedding(x[0])
- out_bigram = self.embedding_ngram2(x[2])
- out_trigram = self.embedding_ngram3(x[3])
- out = torch.cat((out_word, out_bigram, out_trigram), -1)
-
- out = out.mean(dim=1)
- out = self.dropout(out)
- out = self.fc1(out)
- out = F.relu(out)
- out = self.fc2(out)
- return
- # coding: UTF-8
- import torch
- import torch.nn as nn
- import torch.nn.functional as F
- import numpy as np
-
-
- class Config(object):
-
- """配置参数"""
- def __init__(self, dataset, embedding):
- self.model_name = 'TextCNN'
- 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', encoding='utf-8').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 None # 预训练词向量
- self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 设备
-
- self.dropout = 0.5 # 随机失活
- self.require_improvement = 1000 # 若超过1000batch效果还没提升,则提前结束训练
- self.num_classes = len(self.class_list) # 类别数
- self.n_vocab = 0 # 词表大小,在运行时赋值
- self.num_epochs = 20 # epoch数
- self.batch_size = 128 # mini-batch大小
- self.pad_size = 32 # 每句话处理成的长度(短填长切)
- self.learning_rate = 1e-3 # 学习率
- self.embed = self.embedding_pretrained.size(1)\
- if self.embedding_pretrained is not None else 300 # 字向量维度
- self.filter_sizes = (2, 3, 4) # 卷积核尺寸
- self.num_filters = 256 # 卷积核数量(channels数)
-
-
- '''Convolutional Neural Networks for Sentence Classification'''
-
-
- 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)
- self.convs = nn.ModuleList(
- [nn.Conv2d(1, config.num_filters, (k, config.embed)) for k in config.filter_sizes])
- self.dropout = nn.Dropout(config.dropout)
- self.fc = nn.Linear(config.num_filters * len(config.filter_sizes), config.num_classes)
-
- def conv_and_pool(self, x, conv):
- x = F.relu(conv(x)).squeeze(3)
- x = F.max_pool1d(x, x.size(2)).squeeze(2)
- return x
-
- def forward(self, x):
- out = self.embedding(x[0])
- out = out.unsqueeze(1)
- out = torch.cat([self.conv_and_pool(out, conv) for conv in self.convs], 1)
- out = self.dropout(out)
- out = self.fc(out)
- return out
- # coding: UTF-8
- import torch
- import torch.nn as nn
- import torch.nn.functional as F
- import numpy as np
-
-
- class Config(object):
-
- """配置参数"""
- def __init__(self, dataset, embedding):
- self.model_name = 'TextRCNN'
- 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', encoding='utf-8').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 None # 预训练词向量
- self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 设备
-
- self.dropout = 1.0 # 随机失活
- self.require_improvement = 1000 # 若超过1000batch效果还没提升,则提前结束训练
- self.num_classes = len(self.class_list) # 类别数
- self.n_vocab = 0 # 词表大小,在运行时赋值
- self.num_epochs = 10 # epoch数
- self.batch_size = 128 # mini-batch大小
- self.pad_size = 32 # 每句话处理成的长度(短填长切)
- self.learning_rate = 1e-3 # 学习率
- self.embed = self.embedding_pretrained.size(1)\
- if self.embedding_pretrained is not None else 300 # 字向量维度, 若使用了预训练词向量,则维度统一
- self.hidden_size = 256 # lstm隐藏层
- self.num_layers = 1 # lstm层数
-
-
- '''Recurrent Convolutional Neural Networks for Text Classification'''
-
-
- 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)
- self.lstm = nn.LSTM(config.embed, config.hidden_size, config.num_layers,
- bidirectional=True, batch_first=True, dropout=config.dropout)
- self.maxpool = nn.MaxPool1d(config.pad_size)
- self.fc = nn.Linear(config.hidden_size * 2 + config.embed, config.num_classes)
-
- def forward(self, x):
- x, _ = x
- embed = self.embedding(x) # [batch_size, seq_len, embeding]=[64, 32, 64]
- out, _ = self.lstm(embed)
- out = torch.cat((embed, out), 2)
- out = F.relu(out)
- out = out.permute(0, 2, 1)
- out = self.maxpool(out).squeeze()
- out = self.fc(out)
- return out
- # coding: UTF-8
- import torch
- import torch.nn as nn
- import numpy as np
-
-
- class 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', encoding='utf-8').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 None # 预训练词向量
- self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 设备
-
- self.dropout = 0.5 # 随机失活
- self.require_improvement = 1000 # 若超过1000batch效果还没提升,则提前结束训练
- self.num_classes = len(self.class_list) # 类别数
- self.n_vocab = 0 # 词表大小,在运行时赋值
- self.num_epochs = 10 # epoch数
- self.batch_size = 128 # mini-batch大小
- self.pad_size = 32 # 每句话处理成的长度(短填长切)
- self.learning_rate = 1e-3 # 学习率
- self.embed = self.embedding_pretrained.size(1)\
- if self.embedding_pretrained is not None else 300 # 字向量维度, 若使用了预训练词向量,则维度统一
- self.hidden_size = 128 # lstm隐藏层
- self.num_layers = 2 # lstm层数
-
-
- '''Recurrent Neural Network for Text Classification with Multi-Task Learning'''
-
-
- 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)
- 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)
-
- def forward(self, x):
- x, _ = x
- out = self.embedding(x) # [batch_size, seq_len, embeding]=[128, 32, 300]
- out, _ = self.lstm(out)
- out = self.fc(out[:, -1, :]) # 句子最后时刻的 hidden state
- return out
-
- '''变长RNN,效果差不多,甚至还低了点...'''
- # def forward(self, x):
- # x, seq_len = x
- # out = self.embedding(x)
- # _, idx_sort = torch.sort(seq_len, dim=0, descending=True) # 长度从长到短排序(index)
- # _, idx_unsort = torch.sort(idx_sort) # 排序后,原序列的 index
- # out = torch.index_select(out, 0, idx_sort)
- # seq_len = list(seq_len[idx_sort])
- # out = nn.utils.rnn.pack_padded_sequence(out, seq_len, batch_first=True)
- # # [batche_size, seq_len, num_directions * hidden_size]
- # out, (hn, _) = self.lstm(out)
- # out = torch.cat((hn[2], hn[3]), -1)
- # # out, _ = nn.utils.rnn.pad_packed_sequence(out, batch_first=True)
- # out = out.index_select(0, idx_unsort)
- # out = self.fc(out)
- # return out
- # coding: UTF-8
- import torch
- import torch.nn as nn
- import torch.nn.functional as F
- import numpy as np
-
-
- class Config(object):
-
- """配置参数"""
- def __init__(self, dataset, embedding):
- self.model_name = 'TextRNN_Att'
- 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', encoding='utf-8').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 None # 预训练词向量
- self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 设备
-
- self.dropout = 0.5 # 随机失活
- self.require_improvement = 1000 # 若超过1000batch效果还没提升,则提前结束训练
- self.num_classes = len(self.class_list) # 类别数
- self.n_vocab = 0 # 词表大小,在运行时赋值
- self.num_epochs = 10 # epoch数
- self.batch_size = 128 # mini-batch大小
- self.pad_size = 32 # 每句话处理成的长度(短填长切)
- self.learning_rate = 1e-3 # 学习率
- self.embed = self.embedding_pretrained.size(1)\
- if self.embedding_pretrained is not None else 300 # 字向量维度, 若使用了预训练词向量,则维度统一
- self.hidden_size = 128 # lstm隐藏层
- self.num_layers = 2 # lstm层数
- self.hidden_size2 = 64
-
-
- '''Attention-Based Bidirectional Long Short-Term Memory Networks for Relation Classification'''
-
-
- 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)
- self.lstm = nn.LSTM(config.embed, config.hidden_size, config.num_layers,
- bidirectional=True, batch_first=True, dropout=config.dropout)
- self.tanh1 = nn.Tanh()
- # self.u = nn.Parameter(torch.Tensor(config.hidden_size * 2, config.hidden_size * 2))
- self.w = nn.Parameter(torch.zeros(config.hidden_size * 2))
- self.tanh2 = nn.Tanh()
- self.fc1 = nn.Linear(config.hidden_size * 2, config.hidden_size2)
- self.fc = nn.Linear(config.hidden_size2, config.num_classes)
-
- def forward(self, x):
- x, _ = x
- emb = self.embedding(x) # [batch_size, seq_len, embeding]=[128, 32, 300]
- H, _ = self.lstm(emb) # [batch_size, seq_len, hidden_size * num_direction]=[128, 32, 256]
-
- M = self.tanh1(H) # [128, 32, 256]
- # M = torch.tanh(torch.matmul(H, self.u))
- alpha = F.softmax(torch.matmul(M, self.w), dim=1).unsqueeze(-1) # [128, 32, 1]
- out = H * alpha # [128, 32, 256]
- out = torch.sum(out, 1) # [128, 256]
- out = F.relu(out)
- out = self.fc1(out)
- out = self.fc(out) # [128, 64]
- return
- import torch
- import torch.nn as nn
- import torch.nn.functional as F
- import numpy as np
- import copy
-
-
- class Config(object):
-
- """配置参数"""
- def __init__(self, dataset, embedding):
- self.model_name = 'Transformer'
- 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', encoding='utf-8').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 None # 预训练词向量
- self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 设备
-
- self.dropout = 0.5 # 随机失活
- self.require_improvement = 2000 # 若超过1000batch效果还没提升,则提前结束训练
- self.num_classes = len(self.class_list) # 类别数
- self.n_vocab = 0 # 词表大小,在运行时赋值
- self.num_epochs = 20 # epoch数
- self.batch_size = 128 # mini-batch大小
- self.pad_size = 32 # 每句话处理成的长度(短填长切)
- self.learning_rate = 5e-4 # 学习率
- self.embed = self.embedding_pretrained.size(1)\
- if self.embedding_pretrained is not None else 300 # 字向量维度
- self.dim_model = 300
- self.hidden = 1024
- self.last_hidden = 512
- self.num_head = 5
- self.num_encoder = 2
-
-
- '''Attention Is All You Need'''
-
-
- 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)
-
- self.postion_embedding = Positional_Encoding(config.embed, config.pad_size, config.dropout, config.device)
- self.encoder = Encoder(config.dim_model, config.num_head, config.hidden, config.dropout)
- self.encoders = nn.ModuleList([
- copy.deepcopy(self.encoder)
- # Encoder(config.dim_model, config.num_head, config.hidden, config.dropout)
- for _ in range(config.num_encoder)])
-
- self.fc1 = nn.Linear(config.pad_size * config.dim_model, config.num_classes)
- # self.fc2 = nn.Linear(config.last_hidden, config.num_classes)
- # self.fc1 = nn.Linear(config.dim_model, config.num_classes)
-
- def forward(self, x):
- out = self.embedding(x[0])
- out = self.postion_embedding(out)
- for encoder in self.encoders:
- out = encoder(out)
- out = out.view(out.size(0), -1)
- # out = torch.mean(out, 1)
- out = self.fc1(out)
- return out
-
-
- class Encoder(nn.Module):
- def __init__(self, dim_model, num_head, hidden, dropout):
- super(Encoder, self).__init__()
- self.attention = Multi_Head_Attention(dim_model, num_head, dropout)
- self.feed_forward = Position_wise_Feed_Forward(dim_model, hidden, dropout)
-
- def forward(self, x):
- out = self.attention(x)
- out = self.feed_forward(out)
- return out
-
-
- class Positional_Encoding(nn.Module):
- def __init__(self, embed, pad_size, dropout, device):
- super(Positional_Encoding, self).__init__()
- self.device = device
- self.pe = torch.tensor([[pos / (10000.0 ** (i // 2 * 2.0 / embed)) for i in range(embed)] for pos in range(pad_size)])
- self.pe[:, 0::2] = np.sin(self.pe[:, 0::2])
- self.pe[:, 1::2] = np.cos(self.pe[:, 1::2])
- self.dropout = nn.Dropout(dropout)
-
- def forward(self, x):
- out = x + nn.Parameter(self.pe, requires_grad=False).to(self.device)
- out = self.dropout(out)
- return out
-
-
- class Scaled_Dot_Product_Attention(nn.Module):
- '''Scaled Dot-Product Attention '''
- def __init__(self):
- super(Scaled_Dot_Product_Attention, self).__init__()
-
- def forward(self, Q, K, V, scale=None):
- '''
- Args:
- Q: [batch_size, len_Q, dim_Q]
- K: [batch_size, len_K, dim_K]
- V: [batch_size, len_V, dim_V]
- scale: 缩放因子 论文为根号dim_K
- Return:
- self-attention后的张量,以及attention张量
- '''
- attention = torch.matmul(Q, K.permute(0, 2, 1))
- if scale:
- attention = attention * scale
- # if mask: # TODO change this
- # attention = attention.masked_fill_(mask == 0, -1e9)
- attention = F.softmax(attention, dim=-1)
- context = torch.matmul(attention, V)
- return context
-
-
- class Multi_Head_Attention(nn.Module):
- def __init__(self, dim_model, num_head, dropout=0.0):
- super(Multi_Head_Attention, self).__init__()
- self.num_head = num_head
- assert dim_model % num_head == 0
- self.dim_head = dim_model // self.num_head
- self.fc_Q = nn.Linear(dim_model, num_head * self.dim_head)
- self.fc_K = nn.Linear(dim_model, num_head * self.dim_head)
- self.fc_V = nn.Linear(dim_model, num_head * self.dim_head)
- self.attention = Scaled_Dot_Product_Attention()
- self.fc = nn.Linear(num_head * self.dim_head, dim_model)
- self.dropout = nn.Dropout(dropout)
- self.layer_norm = nn.LayerNorm(dim_model)
-
- def forward(self, x):
- batch_size = x.size(0)
- Q = self.fc_Q(x)
- K = self.fc_K(x)
- V = self.fc_V(x)
- Q = Q.view(batch_size * self.num_head, -1, self.dim_head)
- K = K.view(batch_size * self.num_head, -1, self.dim_head)
- V = V.view(batch_size * self.num_head, -1, self.dim_head)
- # if mask: # TODO
- # mask = mask.repeat(self.num_head, 1, 1) # TODO change this
- scale = K.size(-1) ** -0.5 # 缩放因子
- context = self.attention(Q, K, V, scale)
-
- context = context.view(batch_size, -1, self.dim_head * self.num_head)
- out = self.fc(context)
- out = self.dropout(out)
- out = out + x # 残差连接
- out = self.layer_norm(out)
- return out
-
-
- class Position_wise_Feed_Forward(nn.Module):
- def __init__(self, dim_model, hidden, dropout=0.0):
- super(Position_wise_Feed_Forward, self).__init__()
- self.fc1 = nn.Linear(dim_model, hidden)
- self.fc2 = nn.Linear(hidden, dim_model)
- self.dropout = nn.Dropout(dropout)
- self.layer_norm = nn.LayerNorm(dim_model)
-
- def forward(self, x):
- out = self.fc1(x)
- out = F.relu(out)
- out = self.fc2(out)
- out = self.dropout(out)
- out = out + x # 残差连接
- out = self.layer_norm(out)
- return out
TextCNN整体结构
数据处理:所有句子padding成一个长度:seq_len
1.模型输入:
[batch_size, seq_len]
2.经过embedding层:加载预训练词向量或者随机初始化, 词向量维度为embed_size:
[batch_size, seq_len, embed_size]
3.卷积层:NLP中卷积核宽度与embed-size相同,相当于一维卷积。
3个尺寸的卷积核:(2, 3, 4),每个尺寸的卷积核有100个。卷积后得到三个特征图:[batch_size, 100, seq_len-1]
[batch_size, 100, seq_len-2]
[batch_size, 100, seq_len-3]
4.池化层:对三个特征图做最大池化
[batch_size, 100]
[batch_size, 100]
[batch_size, 100]
5.拼接:
[batch_size, 300]
6.全连接:num_class是预测的类别数
[batch_size, num_class]
7.预测:softmax归一化,将num_class个数中最大的数对应的类作为最终预测
[batch_size, 1]
分析:
卷积操作相当于提取了句中的2-gram,3-gram,4-gram信息,多个卷积是为了提取多种特征,最大池化将提取到最重要的信息保留。
图片是单向RNN,看个意思就行
1.模型输入: [batch_size, seq_len]
2.经过embedding层:加载预训练词向量或者随机初始化, 词向量维度为embed_size: [batch_size, seq_len, embed_size]
3.双向LSTM:隐层大小为hidden_size,得到所有时刻的隐层状态(前向隐层和后向隐层拼接)
[batch_size, seq_len, hidden_size * 2]
4.拿出最后时刻的隐层值:
[batch_size, hidden_size * 2]
5.全连接:num_class是预测的类别数
[batch_size, num_class]
6.预测:softmax归一化,将num_class个数中最大的数对应的类作为最终预测
[batch_size, 1]
分析:
LSTM能更好的捕捉长距离语义关系,但是由于其递归结构,不能并行计算,速度慢。
BiLSTM+Attention
1.模型输入: [batch_size, seq_len]
2.经过embedding层:加载预训练词向量或者随机初始化, 词向量维度为embed_size: [batch_size, seq_len, embed_size]
3.双向LSTM:隐层大小为hidden_size,得到所有时刻的隐层状态(前向隐层和后向隐层拼接) [batch_size, seq_len, hidden_size * 2]
4.初始化一个可学习的权重矩阵w
w=[hidden_size * 2, 1]
5.对LSTM的输出进行非线性激活后与w进行矩阵相乘,并经行softmax归一化,得到每时刻的分值:
[batch_size, seq_len, 1]
6.将LSTM的每一时刻的隐层状态乘对应的分值后求和,得到加权平均后的终极隐层值
[batch_size, hidden_size * 2]
7.对终极隐层值进行非线性激活后送入两个连续的全连接层
[batch_size, num_class]
8.预测:softmax归一化,将num_class个数中最大的数对应的类作为最终预测
[batch_size, 1]
分析:
其中4~6步是attention机制计算过程,其实就是对lstm每刻的隐层进行加权平均。比如句长为4,首先算出4个时刻的归一化分值:[0.1, 0.3, 0.4, 0.2],然后
终极h终极=0.1h1+0.3h2+0.4h3+0.2h4
left context是前向RNN的隐层值,right context是后向RNN的隐层值。
1.模型输入: [batch_size, seq_len]
2.经过embedding层:加载预训练词向量或者随机初始化, 词向量维度为embed_size: [batch_size, seq_len, embed_size]
3.双向LSTM:隐层大小为hidden_size,得到所有时刻的隐层状态(前向隐层和后向隐层拼接) [batch_size, seq_len, hidden_size * 2]
4.将embedding层与LSTM输出拼接,并进行非线性激活:
[batch_size, seq_len, hidden_size * 2 + embed_size]
5.池化层:seq_len个特征中取最大的
[batch_size, hidden_size * 2 + embed_size]
6.全连接后softmax
[batch_size, num_class] ==> [batch_size, 1]
分析:
双向LSTM每一时刻的隐层值(前向+后向)都可以表示当前词的前向和后向语义信息,将隐藏值与embedding值拼接来表示一个词;然后用最大池化层来筛选出有用的特征信息。emm...就做了一个池化,就能称之为RCNN...
需要注意的是,我的实现和论文中略有不同,论文中其实用的不是我们平时见的RNN,其实从图中能看出来,看公式的区别:
传统RNN:
正向反向ht正向=f(Wht−1+Uxt)ht反向=f(Wht+1+Uxt)
简单粗暴的FastText
0.用哈希算法将2-gram、3-gram信息分别映射到两张表内。
1.模型输入: [batch_size, seq_len]
2.embedding层:随机初始化, 词向量维度为embed_size,2-gram和3-gram同理:
word: [batch_size, seq_len, embed_size]
2-gram:[batch_size, seq_len, embed_size]
3-gram:[batch_size, seq_len, embed_size]
3.拼接embedding层:
[batch_size, seq_len, embed_size * 3]
4.求所有seq_len个词的均值
[batch_size, embed_size * 3]
5.全连接+非线性激活:隐层大小hidden_size
[batch_size, hidden_size]
6.全连接+softmax归一化:
[batch_size, num_class]==>[batch_size, 1]
分析:
不加N-Gram信息,就是词袋模型,准确率89.59%,加上2-gram和3-gram后准确率92.23%。N-Gram的哈希映射算法见utils_fasttext.py中注释------gram-------处。
N-Gram的词表我设的25W,相对于前面几个模型,这个模型稍慢一点,FastText被我搞成SlowText了。。?但是效果不错呀。。哈哈
有人私信问我这个N-Gram,我大体讲一下。对于N-Gram,我们设定一个词表,词表大小自己定,当然越大效果越好,但是你得结合实际情况,对于2-Gram,5000个字(字表大小)的两两组合有多少种你算算,3-Gram的组合有多少种,组合太多了,词表设大效果是好了,但是机器它受不了啊。
所以N-Gram词表大小的设定是要适中的,那么适中的词表就放不下所有N-Gram了,不同的N-Gram用哈希算法可能会映射到词表同一位置,这确实是个弊端,但是问题不大:5000个字不可能每两个字都两两组合出现,很多两个字是永远不会组成2-Gram的,所以真正出现的N-Gram不会特别多,映射到同一词表位置的N-Gram也就不多了。
N-Gram词表大小我定的25w,比较大了,比小词表训练慢了很多,效果也提升了。这么形容N-Gram词表大小对效果的影响吧:一分价钱1分货,十分价钱1.1分货。
1.模型输入: [batch_size, seq_len]
2.经过embedding层:加载预训练词向量或者随机初始化, 词向量维度为embed_size: [batch_size, seq_len, embed_size]
3.进行卷积,250个尺寸为3的卷积核,论文中称这层为region embedding。
[batch_size, 250, seq_len - 3 + 1]
4.接两层卷积(+relu),每层都是250个尺寸为3的卷积核,(等长卷积,先padding再卷积,保证卷积前后的序列长度不变)
[batch_size, 250, seq_len - 3 + 1]
5.接下来进行上图中小框中的操作。
I. 进行 大小为3,步长为2的最大池化,将序列长度压缩为原来的二分之一。(进行采样)
II. 接两层等长卷积(+relu),每层都是250个尺寸为3的卷积核。
III. I的结果加上II的结果。(残差连接)
重复以上操作,直至序列长度等于1。
[batch_size, 250, 1]
6.全连接+softmax归一化:
[batch_size, num_class]==>[batch_size, 1]
分析:
TextCNN的过程类似于提取N-Gram信息,而且只有一层,难以捕捉长距离特征。
反观DPCNN,可以看出来它的region embedding就是一个去掉池化层的TextCNN,再将卷积层叠加。
每层序列长度都减半(如上图所示),可以这么理解:相当于在N-Gram上再做N-Gram。越往后的层,每个位置融合的信息越多,最后一层提取的就是整个序列的语义信息。
首先介绍 Transformer 的整体结构,下图是 Transformer 用于中英文翻译的整体结构:
Transformer 的整体结构,左图Encoder和右图Decoder
可以看到 Transformer 由 Encoder 和 Decoder 两个部分组成,Encoder 和 Decoder 都包含 6 个 block。Transformer 的工作流程大体如下:
第一步:获取输入句子的每一个单词的表示向量 X,X由单词的 Embedding(Embedding就是从原始数据提取出来的Feature) 和单词位置的 Embedding 相加得到。
Transformer 的输入表示
第二步:将得到的单词表示向量矩阵 (如上图所示,每一行是一个单词的表示 x) 传入 Encoder 中,经过 6 个 Encoder block 后可以得到句子所有单词的编码信息矩阵 C,如下图。单词向量矩阵用 Xn×d 表示, n 是句子中单词个数,d 是表示向量的维度 (论文中 d=512)。每一个 Encoder block 输出的矩阵维度与输入完全一致。
Transformer Encoder 编码句子信息
第三步:将 Encoder 输出的编码信息矩阵 C传递到 Decoder 中,Decoder 依次会根据当前翻译过的单词 1~ i 翻译下一个单词 i+1,如下图所示。在使用的过程中,翻译到单词 i+1 的时候需要通过 Mask (掩盖) 操作遮盖住 i+1 之后的单词。
Transofrmer Decoder 预测
上图 Decoder 接收了 Encoder 的编码矩阵 C,然后首先输入一个翻译开始符 "<Begin>",预测第一个单词 "I";然后输入翻译开始符 "<Begin>" 和单词 "I",预测单词 "have",以此类推。这是 Transformer 使用时候的大致流程,接下来是里面各个部分的细节。
Transformer 中单词的输入表示 x由单词 Embedding 和位置 Embedding (Positional Encoding)相加得到。
Transformer 的输入表示
单词的 Embedding 有很多种方式可以获取,例如可以采用 Word2Vec、Glove 等算法预训练得到,也可以在 Transformer 中训练得到。
Transformer 中除了单词的 Embedding,还需要使用位置 Embedding 表示单词出现在句子中的位置。因为 Transformer 不采用 RNN 的结构,而是使用全局信息,不能利用单词的顺序信息,而这部分信息对于 NLP 来说非常重要。所以 Transformer 中使用位置 Embedding 保存单词在序列中的相对或绝对位置。
位置 Embedding 用 PE表示,PE 的维度与单词 Embedding 是一样的。PE 可以通过训练得到,也可以使用某种公式计算得到。在 Transformer 中采用了后者,计算公式如下:
其中,pos 表示单词在句子中的位置,d 表示 PE的维度 (与词 Embedding 一样),2i 表示偶数的维度,2i+1 表示奇数维度 (即 2i≤d, 2i+1≤d)。使用这种公式计算 PE 有以下的好处:
将单词的词 Embedding 和位置 Embedding 相加,就可以得到单词的表示向量 x,x 就是 Transformer 的输入。
Transformer Encoder 和 Decoder
上图是论文中 Transformer 的内部结构图,左侧为 Encoder block,右侧为 Decoder block。红色圈中的部分为 Multi-Head Attention,是由多个 Self-Attention组成的,可以看到 Encoder block 包含一个 Multi-Head Attention,而 Decoder block 包含两个 Multi-Head Attention (其中有一个用到 Masked)。Multi-Head Attention 上方还包括一个 Add & Norm 层,Add 表示残差连接 (Residual Connection) 用于防止网络退化,Norm 表示 Layer Normalization,用于对每一层的激活值进行归一化。
因为 Self-Attention是 Transformer 的重点,所以我们重点关注 Multi-Head Attention 以及 Self-Attention,首先详细了解一下 Self-Attention 的内部逻辑。
Self-Attention 结构
上图是 Self-Attention 的结构,在计算的时候需要用到矩阵Q(查询),K(键值),V(值)。在实际中,Self-Attention 接收的是输入(单词的表示向量x组成的矩阵X) 或者上一个 Encoder block 的输出。而Q,K,V正是通过 Self-Attention 的输入进行线性变换得到的。
Self-Attention 的输入用矩阵X进行表示,则可以使用线性变阵矩阵WQ,WK,WV计算得到Q,K,V。计算如下图所示,注意 X, Q, K, V 的每一行都表示一个单词。
Q, K, V 的计算
得到矩阵 Q, K, V之后就可以计算出 Self-Attention 的输出了,计算的公式如下:
Self-Attention 的输出
公式中计算矩阵Q和K每一行向量的内积,为了防止内积过大,因此除以 dk 的平方根。Q乘以K的转置后,得到的矩阵行列数都为 n,n 为句子单词数,这个矩阵可以表示单词之间的 attention 强度。下图为Q乘以 KT ,1234 表示的是句子中的单词。
Q乘以K的转置的计算
得到QKT 之后,使用 Softmax 计算每一个单词对于其他单词的 attention 系数,公式中的 Softmax 是对矩阵的每一行进行 Softmax,即每一行的和都变为 1.
对矩阵的每一行进行 Softmax
得到 Softmax 矩阵之后可以和V相乘,得到最终的输出Z。
Self-Attention 输出
上图中 Softmax 矩阵的第 1 行表示单词 1 与其他所有单词的 attention 系数,最终单词 1 的输出 Z1 等于所有单词 i 的值 Vi 根据 attention 系数的比例加在一起得到,如下图所示:
Zi 的计算方法
在上一步,我们已经知道怎么通过 Self-Attention 计算得到输出矩阵 Z,而 Multi-Head Attention 是由多个 Self-Attention 组合形成的,下图是论文中 Multi-Head Attention 的结构图。
Multi-Head Attention
从上图可以看到 Multi-Head Attention 包含多个 Self-Attention 层,首先将输入X分别传递到 h 个不同的 Self-Attention 中,计算得到 h 个输出矩阵Z。下图是 h=8 时候的情况,此时会得到 8 个输出矩阵Z。
多个 Self-Attention
得到 8 个输出矩阵 Z1 到 Z8 之后,Multi-Head Attention 将它们拼接在一起 (Concat),然后传入一个Linear层,得到 Multi-Head Attention 最终的输出Z。
Multi-Head Attention 的输出
可以看到 Multi-Head Attention 输出的矩阵Z与其输入的矩阵X的维度是一样的。
Transformer Encoder block
上图红色部分是 Transformer 的 Encoder block 结构,可以看到是由 Multi-Head Attention, Add & Norm, Feed Forward, Add & Norm 组成的。刚刚已经了解了 Multi-Head Attention 的计算过程,现在了解一下 Add & Norm 和 Feed Forward 部分。
Add & Norm 层由 Add 和 Norm 两部分组成,其计算公式如下:
Add &amp;amp;amp; Norm 公式
其中 X表示 Multi-Head Attention 或者 Feed Forward 的输入,MultiHeadAttention(X) 和 FeedForward(X) 表示输出 (输出与输入 X 维度是一样的,所以可以相加)。
Add指 X+MultiHeadAttention(X),是一种残差连接,通常用于解决多层网络训练的问题,可以让网络只关注当前差异的部分,在 ResNet 中经常用到:
残差连接
Norm指 Layer Normalization,通常用于 RNN 结构,Layer Normalization 会将每一层神经元的输入都转成均值方差都一样的,这样可以加快收敛。
Feed Forward 层比较简单,是一个两层的全连接层,第一层的激活函数为 Relu,第二层不使用激活函数,对应的公式如下。
Feed Forward
X是输入,Feed Forward 最终得到的输出矩阵的维度与X一致。
通过上面描述的 Multi-Head Attention, Feed Forward, Add & Norm 就可以构造出一个 Encoder block,Encoder block 接收输入矩阵 X(n×d) ,并输出一个矩阵 O(n×d) 。通过多个 Encoder block 叠加就可以组成 Encoder。
第一个 Encoder block 的输入为句子单词的表示向量矩阵,后续 Encoder block 的输入是前一个 Encoder block 的输出,最后一个 Encoder block 输出的矩阵就是编码信息矩阵 C,这一矩阵后续会用到 Decoder 中。
Encoder 编码句子信息
Transformer Decoder block
上图红色部分为 Transformer 的 Decoder block 结构,与 Encoder block 相似,但是存在一些区别:
Decoder block 的第一个 Multi-Head Attention 采用了 Masked 操作,因为在翻译的过程中是顺序翻译的,即翻译完第 i 个单词,才可以翻译第 i+1 个单词。通过 Masked 操作可以防止第 i 个单词知道 i+1 个单词之后的信息。下面以 "我有一只猫" 翻译成 "I have a cat" 为例,了解一下 Masked 操作。
下面的描述中使用了类似 Teacher Forcing 的概念,不熟悉 Teacher Forcing 的童鞋可以参考以下上一篇文章Seq2Seq 模型详解。在 Decoder 的时候,是需要根据之前的翻译,求解当前最有可能的翻译,如下图所示。首先根据输入 "<Begin>" 预测出第一个单词为 "I",然后根据输入 "<Begin> I" 预测下一个单词 "have"。
Decoder 预测
Decoder 可以在训练的过程中使用 Teacher Forcing 并且并行化训练,即将正确的单词序列 (<Begin> I have a cat) 和对应输出 (I have a cat <end>) 传递到 Decoder。那么在预测第 i 个输出时,就要将第 i+1 之后的单词掩盖住,注意 Mask 操作是在 Self-Attention 的 Softmax 之前使用的,下面用 0 1 2 3 4 5 分别表示 "<Begin> I have a cat <end>"。
第一步:是 Decoder 的输入矩阵和 Mask 矩阵,输入矩阵包含 "<Begin> I have a cat" (0, 1, 2, 3, 4) 五个单词的表示向量,Mask 是一个 5×5 的矩阵。在 Mask 可以发现单词 0 只能使用单词 0 的信息,而单词 1 可以使用单词 0, 1 的信息,即只能使用之前的信息。
输入矩阵与 Mask 矩阵
第二步:接下来的操作和之前的 Self-Attention 一样,通过输入矩阵X计算得到Q,K,V矩阵。然后计算Q和 KT 的乘积 QKT 。
Q乘以K的转置
第三步:在得到 QKT 之后需要进行 Softmax,计算 attention score,我们在 Softmax 之前需要使用Mask矩阵遮挡住每一个单词之后的信息,遮挡操作如下:
Softmax 之前 Mask
得到 Mask QKT 之后在 Mask QKT上进行 Softmax,每一行的和都为 1。但是单词 0 在单词 1, 2, 3, 4 上的 attention score 都为 0。
第四步:使用 Mask QKT与矩阵 V相乘,得到输出 Z,则单词 1 的输出向量 Z1 是只包含单词 1 信息的。
Mask 之后的输出
第五步:通过上述步骤就可以得到一个 Mask Self-Attention 的输出矩阵 Zi ,然后和 Encoder 类似,通过 Multi-Head Attention 拼接多个输出Zi 然后计算得到第一个 Multi-Head Attention 的输出Z,Z与输入X维度一样。
Decoder block 第二个 Multi-Head Attention 变化不大, 主要的区别在于其中 Self-Attention 的 K, V矩阵不是使用 上一个 Decoder block 的输出计算的,而是使用 Encoder 的编码信息矩阵 C 计算的。
根据 Encoder 的输出 C计算得到 K, V,根据上一个 Decoder block 的输出 Z 计算 Q (如果是第一个 Decoder block 则使用输入矩阵 X 进行计算),后续的计算方法与之前描述的一致。
这样做的好处是在 Decoder 的时候,每一位单词都可以利用到 Encoder 所有单词的信息 (这些信息无需 Mask)。
Decoder block 最后的部分是利用 Softmax 预测下一个单词,在之前的网络层我们可以得到一个最终的输出 Z,因为 Mask 的存在,使得单词 0 的输出 Z0 只包含单词 0 的信息,如下:
Decoder Softmax 之前的 Z
Softmax 根据输出矩阵的每一行预测下一个单词:
Decoder Softmax 预测
这就是 Decoder block 的定义,与 Encoder 一样,Decoder 是由多个 Decoder block 组合而成。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。