接下里我们使用一些深度学习模型,深度学习模型会把机器学习模型的两个阶段联合起来进行end2end学习,即把特征表示和分类一起训练,最后一层进行分类,其余层对输入文本进行特征表示。
我们主要使用了一些经典的深度学习模型,包括FastText、TextCNN、biLSTM、TextGRUCNN等模型,为了增加模型的多样性,每个模型提供了word-level和character-level两个版本(一般来说,word-level效果会更好)。由于数据集做了脱敏处理,没有明文,文本用数字字符串进行了编码,所以暂时不能使用预训练语言模型(如 Bert、RoBerta、ALBert、XLNet等,之后我会还整理一个专栏专门介绍预训练语言模型以及在文本分类上的应用)。
目录
1. 数据预处理
- word-level
if __name__ == '__main__':
#读取训练集和测试集
train_df = pd.read_csv('../../input/train_set.csv')
test_df = pd.read_csv('../../input/test_set.csv')
#训练集
train_char = train_df['article'].values.tolist() #以字为间隔
train_word = train_df['word_seg'].values.tolist() #以词为间隔
train_label = train_df['class'].values - 1 #类别标签 从0开始
#测试集
test_char = test_df['article'].values.tolist()
test_word = test_df['word_seg'].values.tolist()
np.save('../../data/label', train_label)
# np.percentile(train_word_len, [0, 50, 80, 90, 95, 98, 100])
# of labels: 19
# Training set
# [6. 514. 990. 1428. 1949. 2858.48 39759.]
# [50. 842. 1618. 2346. 3201. 4720.96 55804.]
# Test set
# [6. 516. 992. 1429. 1949. 2826. 19755.]
# [50. 842. 1621. 2349. 3207. 4672. 31694.]
MAX_LEN = 2000 #序列最大长度 将每个mini-batch的数据都填充成MAX_LEN 方便并行处理
EMBED_DIM = 300 #词嵌入维度
#可以基于训练数据 预先训练好word2vec或GloVe词向量,二者可以单独使用也可以进行拼接 增加多样性
#由于没有明文 不能使用别人开源的预训练词向量 只能自己训练
EMBED_PATH = '../../data/word_vector_300d.vec' #预训练词向量路径
NUM_WORDS = 359279 #词典大小
tokenizer = Tokenizer(num_words=NUM_WORDS) #使用Keras内置的构建词典的函数 默认用空格分词 (官方已经帮我们分好词 并且用空格连接起来了,不然需要手动先分词)
tokenizer.fit_on_texts(train_word + test_word) #基于训练集和测试集构建词典(一般只基于训练集构建词典)
#把所有序列/文本中的单词 转换为词典中的索引,并填充为同一长度
train_sequence = pad_sequences(tokenizer.texts_to_sequences(train_word), MAX_LEN)
test_sequence = pad_sequences(tokenizer.texts_to_sequences(test_word), MAX_LEN)
word2vec_mapping = {} #词到向量的映射字典
with open(EMBED_PATH, 'r') as f: #读取预训练词向量
lines = f.readlines()[1:]
for line in lines:
line = line.strip()
vals = line.split()
word2vec_mapping[vals[0]] = np.fromiter(vals[1:], dtype=np.float32)
print(f'# of words: {len(word2vec_mapping)}')
oov = 0 #没有预训练词向量的词的个数
embed_mat = np.random.uniform(-0.1, 0.1, (NUM_WORDS + 1, EMBED_DIM)) #初始化词嵌入矩阵 预留出一个填充词PAD
for word, i in tokenizer.word_index.items():
if i > NUM_WORDS:
break
else:
if word in word2vec_mapping: #使用预训练词向量覆盖
embed_mat[i, :] = word2vec_mapping[word]
else:
print(i)
oov += 1
print(f'# of OOV words: {oov}') #不存在预训练词向量的单词数
# 保存处理好的训练和测试序列
np.save('../../data/train_input', train_sequence)
np.save('../../data/test_input', test_sequence)
# 保存初始化的词嵌入矩阵
np.save('../../data/word_embed_mat', embed_mat)
- character-level
if __name__ == '__main__':
#读取原始数据集
train_df = pd.read_csv('../../input/train_set.csv')
test_df = pd.read_csv('../../input/test_set.csv')
train_char = train_df['article'].values.tolist() #以字为间隔
train_word = train_df['word_seg'].values.tolist() #以词为间隔
train_label = train_df['class'].values - 1 #类别标签
test_char = test_df['article'].values.tolist()
test_word = test_df['word_seg'].values.tolist()
np.save('../../data/label', train_label)
# np.percentile(train_word_len, [0, 50, 80, 90, 95, 98, 100])
# of labels: 19
# Training set
# [6. 514. 990. 1428. 1949. 2858.48 39759.]
# [50. 842. 1618. 2346. 3201. 4720.96 55804.]
# Test set
# [6. 516. 992. 1429. 1949. 2826. 19755.]
# [50. 842. 1621. 2349. 3207. 4672. 31694.]
MAX_LEN = 3200 #序列最大长度 将每个mini-batch的数据都填充成MAX_LEN 方便并行处理 character-level序列会更长
EMBED_DIM = 300 #字潜入维度
#可以基于训练数据 预先训练好word2vec或GloVe字向量,二者可以单独使用也可以进行拼接 增加多样性
#由于没有明文 不能使用别人开源的预训练字向量 只能自己训练
EMBED_PATH = '../../data/char_vector_300d_new.vec' #加载预训练字向量
tokenizer = Tokenizer()#使用Keras内置的构建字典的函数 默认用空格分字 (官方已经帮我们分好字 并且用空格连接起来了,不然需要手动先分字)
tokenizer.fit_on_texts(train_char + test_char)#基于训练集和测试集构建字典(一般只基于训练集构建字典)
NUM_WORDS = len([w for w, c in tokenizer.word_counts.items() if c >= 5]) #得到每个字及其频数 过滤掉频数<5的字
print(NUM_WORDS) #统计过滤后字的个数,作为字典的大小
#可以像之前那样根据一些预先的统计信息 直接指定词典的大小(取词频排在前词典大小的词) 也可以先过滤掉低频词,把剩余的词数作为词典大小
tokenizer = Tokenizer(num_words=NUM_WORDS) #构建字典
tokenizer.fit_on_texts(train_char + test_char)
#把所有序列/文本中的字 转换为字典中的索引,并填充为同一长度
train_sequence = pad_sequences(tokenizer.texts_to_sequences(train_char), MAX_LEN)
test_sequence = pad_sequences(tokenizer.texts_to_sequences(test_char), MAX_LEN)
char2vec_mapping = {} #词到向量的映射字典
with open(EMBED_PATH, 'r') as f: #读取预训练字向量
lines = f.readlines()[1:]
for line in lines:
line = line.strip()
vals = line.split()
char2vec_mapping[vals[0]] = np.fromiter(vals[1:], dtype=np.float32)
print(f'# of chars: {len(char2vec_mapping)}')
oov = 0
embed_mat = np.random.uniform(-0.1, 0.1, (len(char2vec_mapping) + 1, EMBED_DIM)).astype(np.float32) #初始化字嵌入矩阵 预留出一个填充词PAD
for char, i in tokenizer.word_index.items():
if i > NUM_WORDS:
break
else:
if char in char2vec_mapping:
embed_mat[i, :] = char2vec_mapping[char] #使用预训练字向量覆盖
else:
oov += 1
print(f'# of OOV words: {oov}') #不存在预训练字向量的字数
# 保存处理好的训练和测试序列 character-level
np.save('../../data/char_train_input', train_sequence)
np.save('../../data/char_test_input', test_sequence)
# 保存初始化的字嵌入矩阵
np.save('../../data/char_embed_mat', embed_mat)
2. FastText
- FastText word-level
class FastText(nn.Module):
def __init__(self, fc_dim1, fc_dim2, granularity='word'):
super(FastText, self).__init__()
# 加载初始化的词嵌入矩阵
embed_mat = torch.from_numpy(np.load(f'../../data/{granularity}_embed_mat.npy').astype(np.float32))
num_word, embed_dim = embed_mat.size()
self.embed = nn.Embedding.from_pretrained(embed_mat, False)
#两个隐层
self.fc1 = nn.Linear(embed_dim, fc_dim1)
self.fc2 = nn.Linear(fc_dim1, fc_dim2)
#输出层
self.out = nn.Linear(fc_dim2, 19)
#激活函数
self.act = nn.RReLU()
#BN层
self.bn1 = nn.BatchNorm1d(fc_dim1)
self.bn2 = nn.BatchNorm1d(fc_dim2)
def forward(self, input):
out = self.embed(input) #(batch_size,max_len) -> (batch_size,max_len,embed_size)
out = torch.mean(out, dim=1) #(batch_size,embed_size)
#使用Functional中的Dropout
#也可以把Dropout声明为层 使用更方便 丢弃率为0.5
out = self.bn1(F.dropout(self.act(self.fc1(out)), p=0.5, training=self.training, inplace=True))#(batch_size,fc_dim1)
out = self.bn2(F.dropout(self.act(self.fc2(out)), p=0.5, training=self.training, inplace=True))#(batch_size,fc_dim2)
out = self.out(out)#(batch_size,19)
return out
def _initialize_weights(self): #自定义参数初始化方式
for m in self.modules():
if isinstance(m, nn.Conv2d): #卷积层 权重初始化方式
nn.init.kaiming_normal_(m.weight, mode='fan_out')
if m.bias is not None: #卷积层偏置参数初始化为0
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.BatchNorm2d): #BN层权重初始化为1 偏置初始化为0
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Linear): #全连接层 权重用xavier,偏置初始化为0
nn.init.xavier_uniform_(m.weight)
nn.init.constant_(m.bias, 0)
- FastAttentionText word-level
原始的FastText是将序列中,每个词的词向量直接取平均;FastAttentionText通过Attention机制计算序列中每个词对应的权重,对每个词的词向量进行加权求和。再接几个全连接层做分类。
class Fast_Attention_Text(nn.Module):
def __init__(self, fc_dim1, fc_dim2):
super(Fast_Attention_Text, self).__init__()
self.device = 'cuda:0' if torch.cuda.is_available() else 'cpu'
# 加载初始化的词嵌入矩阵
embed_mat = torch.from_numpy(np.load(f'../../data/word_embed_mat.npy').astype(np.float32))
num_word, embed_dim = embed_mat.size()
self.embed = nn.Embedding.from_pretrained(embed_mat, False) #False表示不冻结 进行微调
#初始化一参数向量 作为Query
self.ctx_vec_size = (300,)
self.ctx_vec = nn.Parameter(torch.randn(self.ctx_vec_size).float().to(self.device))
#先定义一个全连接层/映射层
self.proj = nn.Linear(in_features=300, out_features=300)
#第一个隐层
self.fc1 = nn.Linear(embed_dim, fc_dim1)
#第2个隐层
self.fc2 = nn.Linear(fc_dim1, fc_dim2)
#输出层
self.out = nn.Linear(fc_dim2, 19)
#激活函数
self.act = nn.RReLU()
#BN层
self.bn1 = nn.BatchNorm1d(fc_dim1)
self.bn2 = nn.BatchNorm1d(fc_dim2)
def