一、写在前面的话
最近项目需要做一个针对内容的打标签系统,这里的内容是CSDN网站上面用户创作的内容,例如,博客、问答等,打上CSDN统一标签之后有利于对内容的归类和检索,即知识的结构化。
CSDN统一标签目前大概有400-500个,有大类和小类两个层级,对于python这个大类来说,下面的小类有:python,list,django,virtualenv,tornado,flask等标签。大家都知道每个博客的标签数量是不固定的,有可能是1个也有可能是多个,所以这里是一个多标签分类的场景。
博客数据拥有用户自己打的标签,有一些还是挺准确的,训练数据的获取还是比较容易的。模型也比较简单,下面直接开始介绍吧。
二、模型部分
2.1 框架选择
工业界的话选择tensorflow,开源,其生态比较完善,不论模型的互转,还是推理引擎的支持,其都有相应的项目支持,并且tensorflow2.0也开始支持动态图,习惯用pytorch做模型的小伙伴也不妨尝试一下。
2.2 模型搭建
这里选择在textcnn的基础之上进行改进,不熟悉textcnn的可以先自行百度一下,至于为什么选择textcnn,也是因为在众多可选的分类器中,textcnn应该是性价比最高的一款了,一般来说,其效果好于机器学习分类算法,例如svm,但是比bert等预训练模型又差一些。同时考虑到工程应用要考虑到推理速度,硬件成本等,textcnn就成了首选。
首先来看一下textcnn的基本结构,这里借用论文A Sensitivity Analysis of (and Practitioners’ Guide to) Convolutional Neural Networks for Sentence Classification的结构图来进行说明:
这个图其实已经非常好理解了,前面的几层就不多说,主要是enbedding层和卷积层部分,直接看改造部分,原本的textcnn在全连接层之后经过softmax就可以得到每个类别的概率,取概率最高的一个的话就是一个单标签多分类器,现在让全连接层输出num_class*2,然后reshape成batch_size*num_class*2,即让每个类别单独做一个二分类,计算每个二分类loss。最后两层改造之后的结构为:
这里直接使用tensorflow2的动态图(sub-class model)尝试搭建模型,配置如下:
class BlogTagClassifyConfig(object):
# Data loading params
file_train_set = "./data/pro/datasets/tags/blog/recommend/train.txt"
file_dev_set = "./data/pro/datasets/tags/blog/recommend/dev.txt"
out_dir = "./data/pro/models/tag/"
# Model Hyperparameters
vocab_size = 0
embedding_dim = 300
dropout_rate = 0.6
num_classes = 50
regularizers_lambda = 0.2
filter_sizes = "2,3,4"
num_filters = 128
seq_length = 120
learning_rate = 3e-4
# Training parameters
batch_size = 256
num_epochs = 100
evaluate_every = 100
print_every = 50
# 阈值
threshold = 0.8
模型:
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@Time : 2021/07/07
@Author : clong
@Descript: 博客标签分类模型
'''
import tensorflow as tf
from tensorflow import keras
class TextCNN(tf.keras.Model):
"""
TextCNN模型
"""
def __init__(self, config):
super(TextCNN, self).__init__()
self.config = config
self.embedding = tf.keras.layers.Embedding(self.config.vocab_size+1,
self.config.embedding_dim,
mask_zero=True,
input_length=self.config.seq_length,
name='embedding')
self.add_channel = tf.keras.layers.Reshape((self.config.seq_length, self.config.embedding_dim, 1), name='add_channel')
self.conv_pool = self.build_conv_pool()
self.dropout = tf.keras.layers.Dropout(self.config.dropout_rate, name='dropout')
self.dense = tf.keras.layers.Dense(self.config.num_classes*2,
kernel_regularizer=tf.keras.regularizers.l2(self.config.regularizers_lambda),
bias_regularizer=tf.keras.regularizers.l2(self.config.regularizers_lambda),
name='dense')
self.flatten = tf.keras.layers.Flatten(data_format='channels_last', name='flatten')
self.reshape = tf.keras.layers.Reshape((self.config.num_classes, 2), name='reshape')
self.optimizer = tf.keras.optimizers.Adam(learning_rate=self.config.learning_rate)
def build_conv_pool(self):
def conv_pool(embed):
pool_outputs = []
for filter_size in list(map(int, self.config.filter_sizes.split(','))):
filter_shape = (self.config.filter_size, self.config.embedding_dim)
conv = keras.layers.Conv2D(self.config.num_filters, filter_shape, strides=(1, 1), padding='valid',
data_format='channels_last', activation='relu',
kernel_initializer='glorot_normal',
bias_initializer=keras.initializers.constant(0.1),
name='convolution_{:d}'.format(filter_size))(embed)
max_pool_shape = (self.config.seq_length - filter_size + 1, 1)
pool = keras.layers.MaxPool2D(pool_size=max_pool_shape,
strides=(1, 1), padding='valid',
data_format='channels_last',
name='max_pooling_{:d}'.format(filter_size))(conv)
pool_outputs.append(pool)
return pool_outputs
return conv_pool
@tf.function
def call(self, x, training=None):
x = self.embedding(x)
x = self.add_channel(x)
x = self.conv_pool(x)
x = tf.keras.layers.concatenate(x, axis=-1, name='concatenate')
x = self.flatten(x)
x = self.dropout(x, training)
x = self.dense(x)
x = self.reshape(x)
x = tf.nn.softmax(x, axis=-1, name="softmax")
return x
三、数据部分
3.1 数据获取
数据采用博客数据,由于数据的不平衡性,现在取50个类别进行验证,每个类别取2000条数据。
3.2 词典构造
如果有领域词典的话,最好将领域词典和标签加入分词自定义词典,对分词后的博客数据统计词频,按照词频进行排序,这里取词频大于10的60000个词。为了防止漏掉标签词特征,可以将标签也加入到词典中。
这里也可以采用两外两种方式进行尝试:分字,采用字符级的词典,这样词典就会比较小,但是可能训练收敛的速度就会慢一些。另外一种就是特征选择算法挑选特征构建词典,例如卡方验证,信息增益等等(计算量大)。
对于词典中未登录的词(unknown words),网上一般做法都是在词典添加[UNK]词或者随机选择一个未知词,我们的场景只是一个分类模型,不会涉及太多的语义,直接去掉未登录的词,做简化处理。
四、附加策略
由于数据的原因,分类器也不可能百分百达正确,有些时候,也有一些漏掉的情况,例如标题出现了标签的名称,但是分类器却没有打上相关的标签,由于IT行业词汇没有什么歧义,这里可以利用标题和用户自定义标签,对它们进行分词,如果这里面出现了标签或者标签的同义词,则打上相关标签。
五、写在最后
多标签分类器其实还要涉及到分类结果的评估,也可以使用编辑距离等来计算相似度,但我这里更加重视模型打上标签的情况,故没采用通用的方法进行评估。
从结果来看,打标签的效果还是可以的,高阈值的标签都是有着很高的相关性。
================2021/12/13================
增加了demo代码:行走的人偶 / textcnn_demo · GIT CODE