前言

这个项目处理的是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 。 更详细的可以查看源码: 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 >>>

更多推荐