基于词向量的句子级情感分类任务(ChnSentiCorp千言数据集)
本项目是基于词向量的句子级情感分类任务,使用的数据集是千言数据集中的ChnSentiCorp。目的是通过这个情感分类任务熟悉词向量的使用方法。
前言
这个项目处理的是NLP的经典任务,文本二分类问题。主要是想通过这个项目来重新熟悉自然语言处理的整个流程。使用的模型十分简单,思想也十分容易理解。难点在于数据的处理的部分,这一块稍微复杂一些。
目前预训练模型在自然语言处理领域十分火热,取得的效果也远超传统模型。预训练模型的出现,大大简化的模型训练的过程,同时也简化了文本的预处理过程。正是由于它的易用性,可能会让人忽视其底层原理。于是希望通过该项目将整个自然语言处理流程梳理一遍,希望有所帮助。
文本预处理流程介绍
我们从输入开始一步一步讲解。
我们的输入的数据是酒店的评论数据,根据评论判断情感是正向还是负向。以下面三条数据为例:
label text_a
1 非常不错的酒店 已经多次入住
0 房间太小。其他的都一般。。。
0 设施很陈旧,卫生间真的有够脏的,没有电梯
首先进行分词,切分成一个个token(为了便于显示,这里用-进行连接):
非常-不错-的-酒店-已经-多次-入住
房间-太小-。-其他-的-都-一般-。-。-。
设施-很-陈旧-,-卫生间-真的-有够-脏-的-,-没有-电梯
换成表格形式更方便看一些
token1 | token2 | token3 | token4 | token5 | token6 | token7 | token8 | token9 | token10 | token11 | token12 | token13 | … |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
非常 | 不错 | 的 | 酒店 | 已经 | 多次 | 入住 | |||||||
房间 | 太小 | 。 | 其他 | 的 | 都 | 一般 | 。 | 。 | 。 | ||||
设施 | 很 | 陈旧 | , | 卫生间 | 真的 | 有够 | 脏 | 的 | , | 没有 | 电梯 |
然后将 token 转换为 id,这一步的目的是根据 id 找到对应的向量表示
token1 | token2 | token3 | token4 | token5 | token6 | token7 | token8 | token9 | token10 | token11 | token12 | token13 | … |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
466 | 8312 | 1 | 1965 | 182 | 1034 | 14303 | |||||||
4646 | 26078 | 2 | 109 | 1 | 124 | 321 | 2 | 2 | 2 | ||||
1235 | 990 | 44353 | 0 | 35693 | 5292 | [UNK] | 52912 | 1 | 0 | 84 | 4672 |
注: [UNK]
属于特殊字符,表示词表中没有这个词,
之后将数据填充到同一个长度
token1 | token2 | token3 | token4 | token5 | token6 | token7 | token8 | token9 | token10 | token11 | token12 | token13 | … |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
466 | 8312 | 1 | 1965 | 182 | 1034 | 14303 | [PAD] | [PAD] | [PAD] | [PAD] | [PAD] | ||
4646 | 26078 | 2 | 109 | 1 | 124 | 321 | 2 | 2 | 2 | [PAD] | [PAD] | ||
1235 | 990 | 44353 | 0 | 35693 | 5292 | [UNK] | 52912 | 1 | 0 | 84 | 4672 |
注:[PAD]
也是特殊字符,表示填充
由于 [UNK]
和 [PAD]
是我们自己定义的,因此,需要在词典和embedding表中加上这两个词。假设字典大小为352221,那么可以定义352221(索引从0开始) 对应 [UNK]
, 定义352222 对应 [PAD]
, 然后在embedding中添加两个维度为向量维度大小的嵌入向量即可,这两个嵌入向量可以随机初始化,也可以全部都设置为0。
最后将 id 替换为对应的向量表示。
就可以得到类似下面的表示形式:
token_id | embedding |
---|---|
466 | [0.12, 0.23, 0.17, 0.65] |
8312 | [0.34, 0.22, 0.54, 0.98] |
1 | [0.33, 0.54, 0.76, 0.89] |
1965 | [0.22, 0.51, 0.26, 0.86] |
182 | [0.53, 0.25, 0.35, 0.46] |
1034 | [0.65, 0.33, 0.23, 0.46] |
14303 | [0.34, 0.45, 0.64, 0.93] |
[PAD] | [0, 0, 0, 0] |
[PAD] | [0, 0, 0, 0] |
[PAD] | [0, 0, 0, 0] |
[PAD] | [0, 0, 0, 0] |
一个句子就可以转为 seq_len x embedding_dim
大小的矩阵。 如果是多个句子, 可以得到 batch_size x seq_len x embedding_dim
大小的矩阵
OK, 现在我们得到了整个输入句子的词向量表示,之后将这个句子输入到我们定义的神经网络模型中去训练就可以了。神经网络按照自己的需求搭建即可,这里不再展开叙述。
词向量介绍
paddlenlp
提供了很多预训练的词向量,我们直接选择合适的进行加载即可。更多的介绍可以查看 预训练词向量
这里我们选取了一个用维基百科训练的词向量, w2v.wiki.target.word-char.dim300
,这个是字词向量混合,既有字也有词。
加载方式非常简单,只需要一行代码即可:
# 注意这个TokenEmbedding跟paddle中的Embedding是有区别的
from paddlenlp.embeddings import TokenEmbedding
embedding = TokenEmbedding('w2v.wiki.target.word-char.dim300', trainable=True)
然而我们的目的是学习原理,仅仅会用是不够的。下面我们进一步了解一下这个词向量。
词向量的下载路径为: https://paddlenlp.bj.bcebos.com/models/embeddings/{}.tar.gz
用自己选择的词向量名字替换括号内容即可。这里我用的是 w2v.wiki.target.word-char.dim300
。 更详细的可以查看源码: embedding 在 constant.py
和 token_embedding.py
两个文件中
然后下载解压即可
!wget https://paddlenlp.bj.bcebos.com/models/embeddings/w2v.wiki.target.word-char.dim300.tar.gz
!tar -xvf w2v.wiki.target.word-char.dim300.tar.gz
接下来看一下文件的内容
import numpy as np
# 加载词向量文件
wiki = np.load('w2v.wiki.target.word-char.dim300.npz')
# 看看有哪些东西,
for val in wiki:
print(val) # 输出中看到 vocab 和 embedding
vocab
embedding
# vocab 指的就是字典,也就是说有哪些字或者词对应着词向量
# 打印一下前100个字典的内容, 这些是出现频率前100的字或词
vocab = wiki['vocab']
print(vocab[:100].tolist())
# 看一下字典的总长度, 总共有352221个字、词、数字、标点符号等等
print(len(vocab))
[',', '的', '。', '、', '平方公里', '和', ':', 'formula_', '在', '“', '一', '与', '了', '》', '一个', '”', '后', '中', '年', '中国', '有', '被', '地区', '及', '以', '人口密度', '人', '于', '他', '也', '而', '由', '《', ')', '10', '可以', '(', '位于', ')', '并', '为', '是', '等', '中华人民共和国', '成为', '12', '人口', '上', '美国', ',', '以及', '使用', '开始', '时', '个', '2009', '1', '第', '将', '日本', '11', '至', '-', '之', '对', '其', '(', '月', '总人口', '乌克兰', ';', '或', '海拔高度', '主要', '到', '包括', '进行', '2', '面积', '会', '总面积', '但', '3', '之后', '没有', '台湾', '变化', '"', '2011', '2000', '两', '其中', '第一', '三', '2001', '认为', '因此', '管辖', '负责', '他们']
352221
# embedding指的就是vocab中的字或词对应的向量
embedding = wiki['embedding']
# 查看embedding的shape 结果为: (352221, 300)
# 其中352221代表词向量的个数,300代表词向量的维度
# vocab跟embedding是一一对应的
print(embedding.shape)
# 查看第一个符号 "," 对应的词向量
print(embedding[0])
(352221, 300)
[ 8.35410e-02 1.14139e-01 -2.92372e-01 -2.84932e-01 1.58744e-01
-6.54680e-02 1.32197e-01 -1.39153e-01 3.09139e-01 -3.05303e-01
3.17440e-01 -7.24250e-02 2.46060e-02 -5.64550e-02 -1.08127e-01
-9.14600e-03 -2.72408e-01 -1.54427e-01 2.01222e-01 1.64735e-01
-4.14140e-02 -2.12002e-01 -2.21503e-01 -7.48310e-02 4.89580e-02
-2.88856e-01 -2.15087e-01 6.99290e-02 2.53270e-02 2.21477e-01
6.22110e-02 -3.44253e-01 2.43120e-01 2.49153e-01 -1.46901e-01
1.66859e-01 2.87860e-02 5.90640e-02 1.17248e-01 1.47364e-01
-2.76340e-02 -6.17360e-02 1.47003e-01 -2.73429e-01 -1.17932e-01
-2.02454e-01 -1.16372e-01 1.50086e-01 6.18980e-02 1.89527e-01
-1.71660e-02 1.17800e-02 -5.18314e-01 1.24517e-01 1.52821e-01
-4.72180e-02 1.27404e-01 1.44860e-02 -1.31538e-01 -2.85258e-01
-2.47738e-01 8.75940e-02 3.69310e-02 -6.00260e-02 -2.36415e-01
-9.85200e-03 -1.39819e-01 -2.21046e-01 -2.67140e-01 2.77820e-02
7.42140e-02 1.18644e-01 2.00961e-01 2.10938e-01 2.91064e-01
-1.29978e-01 2.30790e-02 2.57427e-01 -2.15216e-01 -2.14160e-02
1.97299e-01 1.03367e-01 -2.13221e-01 2.19999e-01 7.53110e-02
-7.53690e-02 -3.78030e-02 9.42330e-02 1.07600e-02 8.24120e-02
5.79000e-04 -7.94970e-02 -5.53509e-01 -1.14336e-01 -6.11900e-02
-5.85800e-02 -1.61716e-01 -7.78600e-03 1.07505e-01 3.63670e-02
-1.56928e-01 8.03120e-02 -3.23166e-01 -1.05452e-01 2.14500e-03
4.42131e-01 -1.70726e-01 -2.95090e-02 1.17284e-01 5.19950e-02
-1.27385e-01 8.84480e-02 1.25887e-01 1.92357e-01 1.78114e-01
8.48520e-02 1.87783e-01 3.73290e-02 -7.44000e-02 3.91794e-01
-7.12610e-02 -7.75240e-02 1.85953e-01 -1.14497e-01 -3.71820e-02
-1.60399e-01 -4.27474e-01 -8.37340e-02 1.06319e-01 -9.77170e-02
3.06224e-01 9.93750e-02 7.95300e-02 9.87330e-02 -9.44370e-02
1.20700e-02 -1.62792e-01 1.78063e-01 -2.11064e-01 4.30690e-02
-1.99111e-01 -6.39790e-02 3.89280e-02 3.89150e-02 2.62006e-01
8.14110e-02 -1.46451e-01 -3.85360e-02 2.29320e-02 -1.72914e-01
-1.68798e-01 5.85350e-02 -8.56700e-02 -2.01629e-01 2.93654e-01
2.80446e-01 1.62925e-01 -1.08380e-01 -2.05419e-01 2.57824e-01
-3.19560e-02 -2.30418e-01 -4.67226e-01 -1.27340e-02 3.83260e-02
-3.64042e-01 6.43600e-02 -1.69722e-01 -1.92244e-01 -7.02847e-01
-2.10916e-01 8.68660e-02 2.84105e-01 7.80940e-02 2.44150e-01
-1.69754e-01 1.60225e-01 1.53577e-01 4.85519e-01 -2.10424e-01
-1.10670e-02 -1.47049e-01 -5.79260e-02 1.00110e-02 7.14100e-02
1.62000e-01 1.12363e-01 -2.46288e-01 -2.48376e-01 -4.84370e-01
-3.89970e-02 -8.43850e-02 -3.21974e-01 -1.18750e-02 1.50366e-01
1.65387e-01 -2.78365e-01 -1.36012e-01 2.43409e-01 9.67120e-02
-8.32730e-02 4.64350e-02 -1.72072e-01 -1.47045e-01 1.82656e-01
-1.13854e-01 4.71520e-02 -1.44201e-01 -9.32680e-02 1.16467e-01
-1.68416e-01 -9.73480e-02 1.22250e-01 1.68727e-01 -3.42652e-01
-1.09534e-01 9.42590e-02 -1.64667e-01 -2.20367e-01 7.49960e-02
-8.80880e-02 -5.76851e-01 4.13600e-03 1.74849e-01 -1.10101e-01
-3.34660e-02 -2.09003e-01 1.66170e-01 3.89760e-02 -4.45090e-02
1.02061e-01 -1.79890e-01 7.31520e-02 2.61225e-01 1.97484e-01
2.27935e-01 3.00610e-02 -1.23717e-01 -5.31270e-02 1.58026e-01
-1.79319e-01 1.19329e-01 4.72050e-02 6.44930e-02 1.78721e-01
1.29672e-01 1.08086e-01 2.64697e-01 5.72280e-02 -9.37620e-02
1.14071e-01 2.68889e-01 7.77110e-02 -6.47900e-03 9.56000e-03
-7.02140e-02 -3.19790e-01 2.50036e-01 -4.43970e-02 2.31661e-01
7.13040e-02 -1.46146e-01 2.24644e-01 2.24700e-01 2.09060e-01
1.03385e-01 -3.12938e-01 2.13922e-01 4.85100e-03 1.56542e-01
1.44845e-01 1.12427e-01 7.28700e-03 1.45220e-02 2.25405e-01
-1.12594e-01 1.75400e-01 -1.08501e-01 9.95600e-02 -2.60275e-01
-3.10369e-01 1.19924e-01 3.15709e-01 1.01501e-01 2.54773e-01
6.95170e-02 1.13821e-01 -2.80870e-02 2.11021e-01 -1.76172e-01
-1.20230e-01 6.03200e-02 2.15980e-01 -9.55190e-02 -2.71288e-01
2.24406e-01 -3.67598e-01 -1.77254e-01 2.78266e-01 -4.25315e-01]
import paddle
# 上面我们已经获取了embedding矩阵,下面我们就用这个embedding矩阵初始化paddle的Embedding层
# 首先创建一个参数属性对象,并将我们的embedding进行初始化
weight = paddle.ParamAttr(initializer=paddle.nn.initializer.Assign(embedding))
# 创建embedding层,权重就是用我们上面定的权重
embedding_layer = paddle.nn.Embedding(num_embeddings=embedding.shape[0], # 有多少个嵌入向量
embedding_dim=embedding.shape[1], # 每个嵌入向量的维度是多少
weight_attr=weight) # 初始化嵌入向量层
# 查看一下是不是用我们的embedding进行初始化的。
# 对比字典中第一个字的嵌入向量表示,可以发现,他们是一样的。(跟上面的输出对比,都输出了第一个向量)
print(embedding_layer.weight[0])
Tensor(shape=[300], dtype=float32, place=CUDAPlace(0), stop_gradient=False,
[ 0.08354100, 0.11413900, -0.29237199, -0.28493199, 0.15874401, -0.06546800, 0.13219699, -0.13915300, 0.30913901, -0.30530301, 0.31744000, -0.07242500, 0.02460600, -0.05645500, -0.10812700, -0.00914600, -0.27240801, -0.15442701, 0.20122200, 0.16473500, -0.04141400, -0.21200199, -0.22150300, -0.07483100, 0.04895800, -0.28885600, -0.21508700, 0.06992900, 0.02532700, 0.22147700, 0.06221100, -0.34425300, 0.24312000, 0.24915300, -0.14690100, 0.16685900, 0.02878600, 0.05906400, 0.11724800, 0.14736401, -0.02763400, -0.06173600, 0.14700300, -0.27342901, -0.11793200, -0.20245400, -0.11637200, 0.15008600, 0.06189800, 0.18952700, -0.01716600, 0.01178000, -0.51831400, 0.12451700, 0.15282100, -0.04721800, 0.12740400, 0.01448600, -0.13153800, -0.28525800, -0.24773800, 0.08759400, 0.03693100, -0.06002600, -0.23641500, -0.00985200, -0.13981900, -0.22104600, -0.26714000, 0.02778200, 0.07421400, 0.11864400, 0.20096099, 0.21093801, 0.29106399, -0.12997800, 0.02307900, 0.25742701, -0.21521600, -0.02141600, 0.19729900, 0.10336700, -0.21322100, 0.21999900, 0.07531100, -0.07536900, -0.03780300, 0.09423300, 0.01076000, 0.08241200, 0.00057900, -0.07949700, -0.55350900, -0.11433600, -0.06119000, -0.05858000, -0.16171600, -0.00778600, 0.10750500, 0.03636700, -0.15692800, 0.08031200, -0.32316601, -0.10545200, 0.00214500, 0.44213101, -0.17072600, -0.02950900, 0.11728400, 0.05199500, -0.12738501, 0.08844800, 0.12588701, 0.19235700, 0.17811400, 0.08485200, 0.18778300, 0.03732900, -0.07440000, 0.39179400, -0.07126100, -0.07752400, 0.18595301, -0.11449700, -0.03718200, -0.16039900, -0.42747399, -0.08373400, 0.10631900, -0.09771700, 0.30622399, 0.09937500, 0.07953000, 0.09873300, -0.09443700, 0.01207000, -0.16279200, 0.17806301, -0.21106400, 0.04306900, -0.19911100, -0.06397900, 0.03892800, 0.03891500, 0.26200601, 0.08141100, -0.14645100, -0.03853600, 0.02293200, -0.17291400, -0.16879800, 0.05853500, -0.08567000, -0.20162900, 0.29365399, 0.28044599, 0.16292500, -0.10838000, -0.20541900, 0.25782400, -0.03195600, -0.23041800, -0.46722600, -0.01273400, 0.03832600, -0.36404201, 0.06436000, -0.16972201, -0.19224399, -0.70284700, -0.21091600, 0.08686600, 0.28410500, 0.07809400, 0.24415000, -0.16975400, 0.16022500, 0.15357700, 0.48551899, -0.21042401, -0.01106700, -0.14704899, -0.05792600, 0.01001100, 0.07141000, 0.16200000, 0.11236300, -0.24628800, -0.24837600, -0.48436999, -0.03899700, -0.08438500, -0.32197401, -0.01187500, 0.15036599, 0.16538700, -0.27836499, -0.13601200, 0.24340899, 0.09671200, -0.08327300, 0.04643500, -0.17207199, -0.14704500, 0.18265601, -0.11385400, 0.04715200, -0.14420100, -0.09326800, 0.11646700, -0.16841599, -0.09734800, 0.12225000, 0.16872700, -0.34265199, -0.10953400, 0.09425900, -0.16466700, -0.22036700, 0.07499600, -0.08808800, -0.57685101, 0.00413600, 0.17484900, -0.11010100, -0.03346600, -0.20900300, 0.16617000, 0.03897600, -0.04450900, 0.10206100, -0.17989001, 0.07315200, 0.26122499, 0.19748400, 0.22793500, 0.03006100, -0.12371700, -0.05312700, 0.15802599, -0.17931899, 0.11932900, 0.04720500, 0.06449300, 0.17872100, 0.12967201, 0.10808600, 0.26469699, 0.05722800, -0.09376200, 0.11407100, 0.26888901, 0.07771100, -0.00647900, 0.00956000, -0.07021400, -0.31979001, 0.25003600, -0.04439700, 0.23166101, 0.07130400, -0.14614600, 0.22464401, 0.22470000, 0.20906000, 0.10338500, -0.31293800, 0.21392199, 0.00485100, 0.15654200, 0.14484499, 0.11242700, 0.00728700, 0.01452200, 0.22540499, -0.11259400, 0.17540000, -0.10850100, 0.09956000, -0.26027501, -0.31036901, 0.11992400, 0.31570899, 0.10150100, 0.25477299, 0.06951700, 0.11382100, -0.02808700, 0.21102101, -0.17617200, -0.12023000, 0.06032000, 0.21597999, -0.09551900, -0.27128801, 0.22440600, -0.36759800, -0.17725401, 0.27826601, -0.42531499])
OK, 到这里我们就将如何使用自己的预训练词向量说完了。下面我们看看词向量获取之后该继续怎么做
文本预处理简单案例
首先要明白我们的目的:模型的输入是一个完整的句子,我们需要将它转换为对应的词向量表示
以下面这句话为例:
跟住招待所没什么太大区别。 绝对不会再住第2次的酒店!
import jieba
# 1. 创建字典
dictionary = {val: index for index, val in enumerate(vocab)}
count = 0
# 查看字典中的10个字跟对应的id
for k, v in dictionary.items():
print(k, v)
if count == 10:
break
count += 1
# 2. 分词
res = jieba.lcut('跟住招待所没什么太大区别。 绝对不会再住第2次的酒店!')
print('分词结果: ', res)
# 3. 在字典中查找token对应的索引
print(dictionary.get('跟'))
print(dictionary.get('住'))
print(dictionary.get('招待所'))
print(dictionary.get('没什么'))
print(dictionary.get('区别'))
# 4. 根据索引查找对应的embedding
print('第一个字对应的embedding')
print(embedding[1697])
Building prefix dict from the default dictionary ...
, 0
的 1
。 2
、 3
平方公里 4
和 5
: 6
formula_ 7
在 8
“ 9
一 10
Dumping model to file cache /tmp/jieba.cache
Loading model cost 0.889 seconds.
Prefix dict has been built successfully.
分词结果: ['跟', '住', '招待所', '没什么', '太', '大', '区别', '。', ' ', '绝对', '不会', '再', '住', '第', '2', '次', '的', '酒店', '!']
1697
6192
23675
19291
4061
第一个字对应的embedding
[ 2.13595e-01 -2.28633e-01 -2.16519e-01 -3.08220e-02 -2.59479e-01
2.79534e-01 8.38670e-02 1.84530e-02 1.03011e-01 -1.14151e-01
1.71487e-01 -2.43329e-01 9.86400e-02 1.77410e-01 -6.74900e-03
-3.28620e-02 -4.13406e-01 1.86486e-01 4.86780e-02 1.56191e-01
1.69024e-01 -1.48089e-01 -2.09578e-01 -4.74126e-01 -4.26700e-03
9.45400e-03 3.91760e-02 9.01400e-03 -1.11487e-01 1.26245e-01
7.07070e-02 -2.42983e-01 7.96740e-02 9.93830e-02 -1.83901e-01
3.30861e-01 -2.47190e-02 1.25099e-01 1.75979e-01 2.79365e-01
-4.42410e-02 5.48240e-02 5.30610e-02 -1.36230e-01 1.37668e-01
-9.63940e-02 3.21554e-01 -1.93180e-02 2.17682e-01 7.55380e-02
-1.32552e-01 2.68070e-02 -2.24863e-01 1.39530e-01 1.65447e-01
-2.77727e-01 1.00418e-01 -5.15400e-03 1.38631e-01 -4.46630e-02
-7.06300e-03 -1.60300e-03 2.95809e-01 -4.33090e-02 -3.28390e-02
-1.64381e-01 2.60170e-02 4.06250e-02 1.08324e-01 2.22224e-01
3.16655e-01 -2.83943e-01 3.20266e-01 -5.55170e-02 2.78946e-01
-5.67660e-02 -3.64660e-02 5.05080e-02 3.43445e-01 -1.01648e-01
1.62623e-01 -1.07527e-01 -2.22405e-01 2.37400e-02 -1.62180e-02
3.59057e-01 -3.38499e-01 -1.60886e-01 1.13725e-01 -2.49370e-02
1.89530e-02 -2.29060e-02 5.86870e-02 -1.09936e-01 3.59580e-02
1.96484e-01 -2.27166e-01 5.74220e-02 1.69553e-01 -3.69352e-01
5.23900e-02 -9.42170e-02 -3.50983e-01 -1.18177e-01 1.44993e-01
-3.91670e-02 -1.72630e-02 -2.72340e-02 1.47170e-02 -3.55620e-02
5.06890e-02 1.60269e-01 -9.86570e-02 1.83370e-02 -3.03140e-02
-1.87724e-01 2.49601e-01 -5.86930e-02 2.18633e-01 1.48454e-01
-1.47195e-01 2.81180e-02 1.02429e-01 -2.88019e-01 2.15394e-01
8.30470e-02 -2.54601e-01 -1.81460e-02 1.10463e-01 1.86560e-01
5.76400e-03 -4.09473e-01 1.73419e-01 -1.86995e-01 1.42321e-01
3.82792e-01 1.98348e-01 -3.29813e-01 -1.28241e-01 -1.27267e-01
2.35623e-01 -5.11560e-02 -3.32649e-01 5.90270e-02 3.90080e-02
1.76063e-01 -4.37910e-02 8.44450e-02 1.11960e-02 -6.02840e-02
2.78668e-01 -5.65800e-02 -1.27927e-01 9.99500e-03 -1.38059e-01
1.00764e-01 2.36051e-01 3.49034e-01 -2.47256e-01 3.19831e-01
3.68471e-01 -1.56747e-01 -1.19395e-01 2.15920e-02 2.41686e-01
-1.80163e-01 4.83340e-02 -2.24770e-02 -3.93575e-01 -5.45841e-01
-2.40240e-02 -1.11680e-01 2.48218e-01 -6.78110e-02 3.02921e-01
-7.79430e-02 -1.35609e-01 -2.03034e-01 2.75932e-01 -3.49121e-01
-2.35570e-02 -1.62055e-01 7.49650e-02 -4.30380e-02 2.82790e-02
-1.25875e-01 4.11560e-02 -1.32629e-01 2.14893e-01 -2.52403e-01
-1.11090e-01 3.07354e-01 -6.12932e-01 -2.10180e-02 -2.29907e-01
1.79654e-01 2.82380e-02 -3.98175e-01 3.63600e-02 3.52793e-01
-1.44086e-01 -2.24130e-02 -2.68170e-02 -1.40726e-01 -1.60002e-01
2.45654e-01 1.25692e-01 -1.26653e-01 -1.06027e-01 2.18510e-02
-1.93178e-01 -1.33040e-01 5.93130e-02 1.05426e-01 1.94150e-01
-1.30358e-01 1.58323e-01 1.96825e-01 -2.61507e-01 1.49928e-01
-3.30472e-01 -3.03971e-01 -1.39930e-01 1.14640e-02 -1.29590e-01
8.91000e-04 1.88484e-01 -5.36520e-02 -7.11400e-02 2.80915e-01
-7.86060e-02 2.11664e-01 1.42554e-01 -2.59883e-01 -7.66260e-02
6.11000e-04 2.14158e-01 -2.08156e-01 -5.14892e-01 1.60416e-01
-1.10700e-01 -8.01000e-03 4.10049e-01 -1.53521e-01 3.72758e-01
3.05612e-01 -9.78830e-02 2.39380e-01 -1.16909e-01 -3.63431e-01
2.38980e-02 4.33160e-01 1.73620e-02 2.05882e-01 2.86037e-01
-6.95830e-02 -3.09119e-01 3.51714e-01 4.19070e-02 -1.58358e-01
7.56240e-02 -1.02514e-01 1.45196e-01 1.02542e-01 1.11745e-01
1.47589e-01 3.26950e-02 3.33229e-01 5.37520e-02 4.71387e-01
-6.34910e-02 -5.08790e-02 3.54438e-01 3.76260e-02 3.88329e-01
-3.59168e-01 -3.08660e-02 1.38631e-01 2.65823e-01 -3.06033e-01
-2.22407e-01 -1.32335e-01 1.10929e-01 1.59544e-01 6.64800e-02
2.88650e-01 1.04166e-01 -5.04720e-02 -7.61830e-02 -7.78530e-02
4.72620e-02 -9.50260e-02 3.03255e-01 1.28728e-01 -2.05372e-01
-1.36282e-01 -2.45080e-01 3.62700e-02 -4.90600e-02 -1.90895e-01]
案例介绍
# 数据集解压
!unzip data/data95103/ChnSentiCorp.zip -d data/data95103/
# 由于paddlenlp版本迭代太快,所以总是要安装或者更新到最新版本
!pip install --upgrade paddlenlp -i https://pypi.org/simple
import paddle
import paddle.nn as nn
import paddle.nn.functional as F
from paddle.io import DataLoader
from paddle.optimizer import AdamW
from paddlenlp.embeddings import TokenEmbedding
from paddlenlp.data import JiebaTokenizer, Stack, Pad, Tuple
from paddlenlp.datasets import load_dataset, MapDataset
from visualdl import LogWriter # 导入可视化模块,可视化模型损失函数
import pandas as pd
from functools import partial
from sklearn.metrics import accuracy_score, f1_score
# 定义配置参数
class Config(object):
def __init__(self):
# 超参数定义
self.epochs = 100
self.lr = 0.001
self.max_seq_len = 256
self.batch_size = 256
# 数据集定义
self.train_path = './data/data95103/ChnSentiCorp/train.tsv'
self.dev_path = './data/data95103/ChnSentiCorp/dev.tsv'
self.test_path = './data/data95103/ChnSentiCorp/test.tsv'
# 模型参数定义
self.embedding_name = 'w2v.wiki.target.word-char.dim300'
self.num_filters = 256
self.dropout = 0.2
self.num_class = 2
# 定义切词器,目的是将句子切词并转换为id,并进行最大句子截断
# 输入句子: 非常不错的酒店 已经多次入住
# 输出: [466, 8312, 1, 1965, 182, 1034, 14303]
# 注意这里仅仅进行了切词和转换为id,没有进行填充
class Tokenizer(object):
def __init__(self, vocab):
self.vocab = vocab
self.tokenizer = JiebaTokenizer(vocab) #定义切词器
self.UNK_TOKEN = '[UNK]'
self.PAD_TOKEN = '[PAD]'
self.pad_token_id = vocab.token_to_idx.get(self.PAD_TOKEN)
# 将文本序列切词并转换为id,并设定句子最大长度,超出将被截断
def text_to_ids(self, text, max_seq_len=512):
input_ids = []
unk_token_id = self.vocab[self.UNK_TOKEN]
for token in self.tokenizer.cut(text):
token_id = self.vocab.token_to_idx.get(token, unk_token_id)
input_ids.append(token_id)
return input_ids[:max_seq_len]
# 定义数据读取
# 这个函数的目的,是作为load_dataset的参数,用来创建Dataset
def read_func(file_path, is_train=True):
df = pd.read_csv(file_path, sep='\t')
for index, row in df.iterrows():
if is_train:
yield {'label': row['label'], 'text_a': row['text_a']}
else:
yield {'text_a': row['text_a']}
# 定义数据预处理函数
# 将输入句子转换为id
def convert_example(example, tokenizer, max_seq_len):
text_a = example['text_a']
text_a_ids = tokenizer.text_to_ids(text_a, max_seq_len)
if 'label' in example: # 如果有label表示是训练集或者验证集,否则是测试集
return text_a_ids, example['label']
else:
return text_a_ids
# 创建配置参数对象
config = Config()
# 定义词向量Layer
embedding = TokenEmbedding(embedding_name=config.embedding_name,
unknown_token='[UNK]',
unknown_token_vector=None,
extended_vocab_path=None,
trainable=True,
keep_extended_vocab_only=False)
# 根据字典定义切词器
tokenizer = Tokenizer(embedding.vocab)
trans_fn = partial(convert_example, tokenizer=tokenizer, max_seq_len=config.max_seq_len)
# 加载数据集
train_dataset = load_dataset(read_func, file_path=config.train_path, is_train=True, lazy=False)
dev_dataset = load_dataset(read_func, file_path=config.dev_path, is_train=True, lazy=False)
test_dataset = load_dataset(read_func, file_path=config.test_path, is_train=False, lazy=False)
# 定义数据预处理函数
train_dataset.map(trans_fn)
dev_dataset.map(trans_fn)
test_dataset.map(trans_fn)
# 这个函数用来对训练集和验证集进行处理,核心目的就是进行padding。将一个mini-batch的句子长度对齐
batchify_fn_1 = lambda samples, fn=Tuple(
Pad(pad_val=tokenizer.pad_token_id, axis=0), # text_a
Stack(), # label
): fn(samples)
# 这个函数用来对测试集进行处理
batchify_fn_2 = lambda samples, fn=Tuple(
Pad(pad_val=tokenizer.pad_token_id, axis=0), # text_a
): fn(samples)
train_loader = DataLoader(
dataset=train_dataset,
batch_size=config.batch_size,
return_list=True,
shuffle=True,
collate_fn=batchify_fn_1
)
dev_loader = DataLoader(
dataset=dev_dataset,
batch_size=config.batch_size,
return_list=True,
shuffle=False,
collate_fn=batchify_fn_1
)
test_loader = DataLoader(
dataset=test_dataset,
batch_size=config.batch_size,
return_list=True,
shuffle=False,
collate_fn=batchify_fn_2
)
# 定义模型
class TextCNN(nn.Layer):
def __init__(self, config, embedding):
super(TextCNN, self).__init__()
# 定义embedding层,使用指定的词向量嵌入名进行创建
self.embedding = embedding
# 定义三个一维卷积, 卷积核分别为3, 5, 7
self.convs = nn.LayerList([nn.Conv2D(1, config.num_filters, (k, self.embedding.embedding_dim)) for k in (2, 3, 4)])
# 定义两个全连接层进行分类
self.fc = nn.Sequential(
nn.Linear(config.num_filters * 3, 128),
nn.ReLU(),
nn.Dropout(config.dropout),
nn.Linear(128, config.num_class)
)
def conv_and_pool(self, x, conv):
x = F.relu(conv(x).squeeze(3))
x = F.max_pool1d(x, x.shape[2]).squeeze(2)
return x
def forward(self, x):
# 输入维度 batch_size x seq_len
x = self.embedding(x)
# 执行卷积操作
x = x.unsqueeze(1) # 增加一个维度 [batch_size, 1, seq_len, embedding_dim]
x = paddle.concat([self.conv_and_pool(x, conv) for conv in self.convs], axis=1)
# 接全连接层进行分类
x = self.fc(x)
return x
# 定义评估函数,主要对验证集进行评估
def evalue(model, dev_loader, epoch):
total_loss = 0.0
total_acc = 0.0
total_f1 = 0.0
model.eval()
for index, (ids, labels) in enumerate(dev_loader):
logits = model(ids)
preds = paddle.argmax(F.softmax(logits, axis=1), axis=1)
y_pred = preds.flatten().numpy()
y_true = labels.flatten().numpy()
acc = accuracy_score(y_pred, y_true)
f1 = f1_score(y_pred, y_true)
total_acc += acc
total_f1 += f1
model.train()
return total_acc / len(dev_loader), total_f1 / len(dev_loader)
# 定义模型优化器和损失函数
model = TextCNN(config, embedding)
optimizer = AdamW(learning_rate=config.lr, parameters=model.parameters())
cirterion = nn.CrossEntropyLoss()
# 开始训练
with LogWriter(logdir="./log") as writer: # 初始化一个记录器
for i in range(config.epochs):
for index, (ids, labels) in enumerate(train_loader):
optimizer.clear_grad()
ids = paddle.to_tensor(ids)
labels = paddle.to_tensor(labels)
preds = model(ids)
loss = cirterion(preds, labels)
loss.backward()
optimizer.step()
# 每训练一个epoch, 对模型评估一次
acc, f1 = evalue(model, dev_loader, i)
writer.add_scalar(tag="acc", step=i, value=acc) # 记录下每一个epoch的准确率
print('Epoch:', i, 'Accuracy:', acc, 'F1:', f1)
整个模型的准确率随训练轮数变化如下图所示:
可以观察到,整个模型训练不到10轮的时候就已经收敛了,之后一直在0.9上下震荡。模型收敛速度较快,可能是因为数据量比较少。
这个图像是根据visualDL绘制的,想要自己试一试的话。可以在整个模型运行完毕之后,点击左侧的可视化->设置logdir->选择log目录->点击下方启动VisualDL服务->打开VisualDL
总结
这个项目的核心在于通过这个情感分析任务,来熟悉词向量的使用。模型的结果是次要的。
不过从结果上看,整个模型的分类准确率其实还是可以的,在验证集上准确率最高能达到91.24%。
还可以通过以下方式改进模型效果:
-
尝试不同的词向量,观察不同的预训练词向量对情感分类任务的影响
-
改进模型结构,这里用的是TextCNN,基于卷积神经网络,还可以尝试LSTM, Transformer等
-
使用预训练模型。
更多推荐
所有评论(0)