0.引言
开门老话:如有雷同算我抄你的,私信;我就是给自己做个笔记,顺带如果能帮到别人算积德行善;文章通俗风格,难免不严谨,大家意会即可;严谨的可以去找论文推导公式看书.…
现在,随处可见的深度学习课程,一抓一大把的github与博客,什么CNN、LSTM、GRU、Attention,各种“几行实现人脸识别”,诶,我说,导包复制粘贴谁还不会呢。个人深感现在学习成本低,浮躁心理明显,抓个PM让ta看一下博客和文档分分钟就可以依赖Pytorch实现LSTM情感分析信不信??
故,个人觉得,会导包训练几个模型不叫会ML、DL,在实际生产中,真正利用大佬的工具优秀、熟练的解决实际问题才算有点意思,或者就像大佬那样,不断创造优质、经典的工具模型。
我目前的工作主要是前一条路线,那么就说说相关感觉吧。拿例子来说更加形象,比如对qq音乐/手百/网易云搜索入口的搜索query进行分类,打上tag方便进行用户行为、产品分析等,表面上看,就是短文本分类么,切词+CNN/GRU-Attention、模型融合balabala一堆就完事,1天工作量?
NAIVE!!实际上挺费事的。
问题1–问题抽象、分类体系设计(设计到抽样、理解数据、流量预估等)
对qq音乐/网易云/手百、抖音搜索入口的搜索query进行分类,涉及到短文本分类,那么单单只看query是否会信息不足呢,信号扩充怎么做,离线/在线;分类体系在语义上是否好分割,设计是否合理呢;每个类别预估的流量有多大,模型是否好学习…
问题2–样本获取(涉及到大量有监督、无监督学习、人工规则、爬虫等)
有了样本,可以几行实现模型训练;要是没有样本呢,如何获取样本、样本扩招、样本提纯、边界样本处理、样本分布,即,进入模型训练前的训练集哪里来;测试集怎么设置最为合理(2-8分割这种模式的准确率真的实用么),都是需要考虑的。上一个实际项目中确立合理类目体系、获取样本、样本提纯占据项目80%以上的时间,训练、玩模型寥寥几周而已。
问题3–模型选择(涉及到对各种模型的理解和工程化应用)
如果有运行时间和资源要求,哪个模型复杂度最低,消耗资源最少,兼顾准确率;哪些适合调研反复使用,哪些线上时效性适用,这涉及到各种模型的理解以及模型工程化、分布式运行调整。
问题4–模型之外的几板斧(涉及到解决实际问题的架构设计)
单纯的模型能解决一部分分类问题,但是有很多情况也是收效甚微,尤其短文本,带有专名的情况,语义信息很弱,比如电影名“延禧宫略”,在电视剧和小说出来之前,很难知道这是电视剧、小说,还以为是游戏攻略打错字了呢…这些情况的泛化,无论啥模型都是一嘴屎…
以上每一个子问题都是深坑,问题2、3和4中,项目用到的所谓ML、DL模型基本是能想到的都用了,毕竟充分有效的利用工具。
只是mark下提醒自己,切不可看了几个github几个论文会训练模型调参就以为自己多能耐了,无论工业界世界问题的复杂还是学术界模型创新的琢磨,都是路漫漫长。
so,好好学习,天天向上,搬砖之余,勿忘提升。
flag立完了,也BB了这么多,这一篇,就来抛个砖引下玉,一般网上大家讨论最多的是问题3的模型部分,那本文主要记录自己对文本分类模型相关的理解。
1.文本表示(字符串->数值)
为什么要文本表示,因为计算机不能直接对文本字符串进行处理,需要数值化。
将字符转化为数字,也可以叫做编码(encode)。
这其实在生活中很常见,比如你的身高、年龄、体重、入学时间、读书年限等等组成一个list [175,26,55,2007,3,…]就可以看做对你这个个体样本的一个编码。同理,也可以对一段文字进行编码。
(1)词频统计编码
-
先理解下这个:
来一条短文本,“我 爱 这 土地 爱 得 深沉”,维护一个词典(简单的可以将你手上的训练文本进行word count,然后取top 50w之类),接下来就可以对上面文本进行编码,假设词典有10个词[“你”,“我”,“爱”,“土地”,“她”,“他”,“得”,“啥”,“这”,“深沉”],上述短文本可以编码为一个10维向量[0,1,2,1,0,0,1,0,1,1],数值分别表示这个词在词典出现的次数。那么,维护一个词典后,任何一句话、一段话就可以转化为一数值个向量。 -
已经文本转化为数值
上述小操作,文本转数值,已经实现了很大的变化。但是有很多问题,需要一些小操作:
=====
a. 有些字符比如"的"、"。“对你进行分类或者其他目标来说,比较没用甚至是噪声,放在词典里去给句子编码不好用,又占位置,那就扔掉吧–常见停顿词去除
=====
b.单纯的统计词频,假设"我"这个词,在每篇文章样本都出现,可能不在你停用词表中,很大可能对分类也没啥作用,那么,你可以加入停顿词去掉,但是还有一些类似的词咋去找呢–ti-idf思想来构建词典(可以看做"任务个性化停顿词去除”,我随意起的名字…)
=====
c.如果字典太大,那么每个文本的向量会过长,而且稀疏向量难以聚类出规律,那么把向量缩短稠密、同时希望尽量较少信息损失吧–降维如 PCA、SVD…
(2)训练生成编码
-
主题模型 LDA 等等…其实个人觉得放在上面词频统计也还凑合吧
=====
a.来来来,通俗很不严谨的说下:很多条文本,假设M条吧,每个文本都按照上述最简单的词频编码,字典是N维,也就是每个文本数据变成N维向量,那么现在所有的数据是不是可以变成一个M x N维的矩阵了呢,而且是稀疏的
=====
b.那么我可不可以将矩阵M x N近似表示为两个矩阵相乘,一个维度为M x K,一个维度为K*N,答案是可以的,那么K这个新的维度可以理解为"主题",前一个矩阵表示"文本"和"主题"的分布关系,后一个矩阵表示"主题"和"词"的分布关系
=====
c.继续,但看M x K ,是不是发现每个"文本"变成了一个K维向量,是不是有点上面稀疏长向量变为稠密向量的赶脚?如果K较小,是不是还有降维的赶脚
=====
d.当然,计算、获得这两个矩阵的方式就有很多很人才的操作了,各种奇技淫巧就不多说了,不然就是公式大乱斗了。 -
word2vec/word-embedding
这玩意儿,能搜索的文章很多,简单粗暴的来说:
我假设每个词是一个N维的稠密向量,初始化,假设一段文本的长度为L,这么用一个矩阵L x N即可表示,或者对L个N维向量相加/取平均得到N维向量表示,然后经过无监督/有监督学习训练,初始化的向量被不断更新,最终的向量就是每个词的编码,也得到一段文本的编码 -
模型深层encode
=====
其实文本分类或者其他任务训练的时候,CNN、LSTM…之流的中间层数值,不都可以看做文本在不同层次和角度上的数值编码(encode)么…拿LSTM文本分类来说,你可以将LSTM最后分类线性层之前整体作为看做一个"高级编码器",这个高级编码器将文本转化为稠密向量输入LR分类器进行分类,和前面所述的"词频统计编码"一个东西,只不过方式上花里胡哨一些。
=====
来段代码一起爽爽(见下文),你可以认为reshaped之前的小操作(embedding、lstm、attention)整个就是一个高级的编码器,输入一段文本生成一个向量,进入一个LR进行分类;之前火的不能CNN图像分类,那么多层的卷积不过是最后将图像encode为一个稠密向量进入分类器,甚至直接encode到label(去掉全连接):
import torch
import torch.nn as nn
from torch.autograd import Variable
from torch.nn import functional as F
class LSTMText_attention(nn.Module):
def __init__(self,config):
super(LSTMText_attention, self).__init__()
self.model_name = "LSTMText_attention"
self.config = config
self.embedding = nn.Embedding(num_embeddings=config.vocab_size,
embedding_dim=config.embedding_size)
self.lstm = nn.LSTM(input_size = config.embedding_size,\
hidden_size = config.hidden_size,
num_layers = config.num_layers,
bias = True,
batch_first = False,
# dropout = 0.5,
bidirectional = True
)
self.W_s1 = nn.Linear(config.hidden_size*2, config.attention_size, bias=True)
self.W_s2 = nn.Linear(config.attention_size, 1)
self.fc = nn.Sequential(
nn.Linear(config.hidden_size*2,config.linear_hidden_size),
nn.BatchNorm1d(config.linear_hidden_size),
nn.ReLU(inplace=True),
nn.Linear(config.linear_hidden_size,config.num_class)
)
def attention(self,lstm_out):
alpha = self.W_s2(F.tanh(self.W_s1(lstm_out.permute(1,0,2))))#batch,text_len,1
final_output = torch.bmm(F.softmax(alpha,dim=1).permute(0, 2, 1), lstm_out.permute(1,0,2))
return final_output
def forward(self, x):
embed_x = self.embedding(x)
out, hidden = self.lstm(embed_x.permute(1,0,2))
attn_out = self.attention(out)
reshaped = attn_out.view(attn_out.size(0), -1)
logits = self.fc((reshaped))
return logits
再说的多一点,kernel-SVM是不是也就是把一个空间的向量表示转化为另一个空间向量表示来方便分类,也是一种encode?
之所以深度学习那么被人推崇,无非是以前的encode,需要数学分析基础、统计知识、专业经验知识,而现在被各种自动学习的高级模型来代替,有种手工操作到机器自动化的转变,类似工业革命的样子?
2.模型(“编码器”的花里胡哨?)
- textCNN/Multi-CNN/Inception:
卷乘神经网络,或者,卷积核这个玩意儿,可以看做是特征滤波器,不严谨的先拿图像分类举个例子:判断一幅画是不是猫,先有很多卷积核去扫描,卷积核1主要看你没有直角类型的边缘,卷积核2主要主要看你有没有圆形的边缘…以此类推,然后,进行第二层卷积,特征升级为有没有鼻子和嘴…,多堆积几层后面的就可以是鼻子的孔形状、指甲的个数…
CNN浅层-多层 可以理解为 低级特征提取后进一步提取组合为高级特征过程;
另外,以前是人工提取鼻子、眼睛等特征,事实上可能存在人为选的特征不合适、或者存在有效特征没挖掘到的情况,但CNN之类的算法实现了特征的自动提取和选择,很大程度解决了上述情况(卷积核的参数都是训练得来)。
继续不严谨的说到文本分类,CNN通过捕捉区域性规律帮助分类,比如水果类别的“皮”总是和“果”联系在一起,而医疗类别的“皮”总是和“肤”组合在一起…
而CNN的几个版本迭代,无非就是越来越深,宽的有层次,减少参数量,防止过拟合,具体的版本差异我之前的博客https://blog.csdn.net/github_38414650/article/details/75315175有讨论过,贴一下自己的代码减少大家重复劳动。
import torch.nn as nn
import torch
class textCNN_Model(nn.Module):
def __init__(self,config):
super(textCNN_Model,self).__init__()
self.is_training = True
self.model_name = "textCNN_Model"
self.dropout_rate = config.dropout_rate
self.num_class = config.num_class
self.use_element = config.use_element
#self.config = config
self.embedding = nn.Embedding(num_embeddings=config.vocab_size,
embedding_dim=config.embedding_size)
self.convs = nn.ModuleList([
nn.Sequential(nn.Conv1d(in_channels=config.embedding_size,
out_channels=config.feature_size,
kernel_size=h),
nn.BatchNorm1d(num_features=config.feature_size),
nn.ReLU(),
nn.Conv1d(in_channels=config.feature_size,
out_channels=config.feature_size,
kernel_size=h),
nn.BatchNorm1d(num_features=config.feature_size),
nn.ReLU(),
nn.MaxPool1d(kernel_size=config.max_text_len-h*2+2))
for h in config.window_sizes
])
self.fc = nn.Linear(in_features=config.feature_size*len(config.window_sizes),
out_features=config.num_class)
def forward(self,x):
embed_x = self.embedding(x)
embed_x = embed_x.permute(0, 2, 1)
out = [conv(embed_x) for conv in self.convs] #out[i]:batch_size x feature_size*1
out = torch.cat(out, dim=1)
out = out.view(-1, out.size(1))
out = self.fc(out)
return out
- RNN/LSTM/GRU
为啥有了CNN这类自动化提取特征的模型,还要有序列/循环神经网络呢。ANN或者CNN前提假设都是:元素之间是相互独立的,输入与输出也是独立的,这对于物体识别、分类是比较使用的;但对于序列数据如语言模型、机器翻译,RNN之流表现更好。
举个极端的例子:假设训练了一个CNN,将英文单词与汉字映射直接联系起来,输入英文单词,输出为对应汉语,如此,去翻译一篇文章,想象一下会出现什么情况;同样,小时候文言文翻译的时候,老师说的最多的一句话就是,“联系上下文”,RNN之类的模型就是应对此类场景。
RNN的基本进化路线与例子参考循环神经网络你需要知道的几个基本概念
回到文本分类,更具体的说,短文本分类,各类CNN以及各类RNN/+Attention得到的分类结果差异并不是很大,或者换句话说,这些模型encode出的信息对于我们这个分类任务来说,都饱和了,提升的点太有限。这里引入Attention主要是希望标注出对类别贡献最大的字词作为积累。
也许长一些的文本,序列会出现相对较大的优势。知乎“看山杯” 夺冠记以及他们git上的代码说明里有记录,加入RNN元素的模型效果好一些,他们这个文本长度可比我们长太多,我截一下他们的结果,不同模型大致感觉就是0.4个percent左右的区别,可能和他们类别太多有关。
model score
CNN_word 0.4103
RNN_word 0.4119
RCNN_word 0.4115
Inceptin_word 0.4109
FastText_word 0.4091
最上面贴了带Attention的LSTM代码,这里就不贴了。
- n-gram的LR/FastText
这次项目中(我们这个项目中大概文本长度就是十个字左右吧,类别大概百类级别),主流的模型都试过。不得不说的是,直接用n-gram后进行词袋模型编码,就是上文中的[0,1,2,1,0,0,1,0,1,1],利用LR分类,得到的效果也就比各种深度模型的效果低一丢丢,不到1%,但是训练和预测消耗的资源以及速度天差地别。
毕竟n-gram就一定程度的表示了上述CNN的局部规律捕捉和RNN的上下文。
FastText的表现也是异常优秀,效果介于LR和DL模型效果之间,相差不是很大,然而训练速度也是极快。
3.结论(“返璞归真”)
对于短文本分类,n-gram的LR/FastText是性价比之首选。
如果不考虑成本,就把什么n-gram+LR/FastText、GRU、CNN之类的模型去做融合吧,效果会提升一下的,毕竟大杀器。
最后的最后,还是那句话,莫要太沉醉于模型,它可以是咱们解决问题的独孤九剑中的一剑,但不是全部,其他八剑也要修炼,应对不同场景,打出组合技。
我这只是短文本分类的一些经历,什么长文本、Seq2Seq、图像之类的有么有同学们精致分享一波哈,尽量减少重复劳动嘛。