前言

这个项目处理的是NLP的经典任务,文本二分类问题。主要是想通过这个项目来重新熟悉自然语言处理的整个流程。使用的模型十分简单,思想也十分容易理解。难点在于数据的处理的部分,这一块稍微复杂一些。

目前预训练模型在自然语言处理领域十分火热,取得的效果也远超传统模型。预训练模型的出现,大大简化的模型训练的过程,同时也简化了文本的预处理过程。正是由于它的易用性,可能会让人忽视其底层原理。于是希望通过该项目将整个自然语言处理流程梳理一遍,希望有所帮助。

文本预处理流程介绍


我们从输入开始一步一步讲解。

我们的输入的数据是酒店的评论数据,根据评论判断情感是正向还是负向。以下面三条数据为例:

label text_a

1 非常不错的酒店 已经多次入住

0 房间太小。其他的都一般。。。

0 设施很陈旧,卫生间真的有够脏的,没有电梯

首先进行分词,切分成一个个token(为了便于显示,这里用-进行连接):

非常-不错-的-酒店-已经-多次-入住

房间-太小-。-其他-的-都-一般-。-。-。

设施-很-陈旧-,-卫生间-真的-有够-脏-的-,-没有-电梯

换成表格形式更方便看一些

token1token2token3token4token5token6token7token8token9token10token11token12token13
非常不错酒店已经多次入住
房间太小其他一般
设施陈旧卫生间真的有够没有电梯

然后将 token 转换为 id,这一步的目的是根据 id 找到对应的向量表示

token1token2token3token4token5token6token7token8token9token10token11token12token13
466831211965182103414303
46462607821091124321222
1235990443530356935292[UNK]5291210844672

注: [UNK] 属于特殊字符,表示词表中没有这个词,

之后将数据填充到同一个长度

token1token2token3token4token5token6token7token8token9token10token11token12token13
466831211965182103414303[PAD][PAD][PAD][PAD][PAD]
46462607821091124321222[PAD][PAD]
1235990443530356935292[UNK]5291210844672

注:[PAD] 也是特殊字符,表示填充

由于 [UNK][PAD] 是我们自己定义的,因此,需要在词典和embedding表中加上这两个词。假设字典大小为352221,那么可以定义352221(索引从0开始) 对应 [UNK] , 定义352222 对应 [PAD], 然后在embedding中添加两个维度为向量维度大小的嵌入向量即可,这两个嵌入向量可以随机初始化,也可以全部都设置为0。

最后将 id 替换为对应的向量表示。

就可以得到类似下面的表示形式:

token_idembedding
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 。 更详细的可以查看源码: embeddingconstant.pytoken_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%。

还可以通过以下方式改进模型效果:

  1. 尝试不同的词向量,观察不同的预训练词向量对情感分类任务的影响

  2. 改进模型结构,这里用的是TextCNN,基于卷积神经网络,还可以尝试LSTM, Transformer等

  3. 使用预训练模型。

Logo

学大模型,用大模型上飞桨星河社区!每天8点V100G算力免费领!免费领取ERNIE 4.0 100w Token >>>

更多推荐