NLP入门之 通过 四大名著 学Embedding

为什么会有embedding出现?

这里我以自己搞图像分类的理解,来代入我们的nlp的Embedding。

在这里插入图片描述

Word2Vec

embedding的进入很难避开Word2Vec。

Word2Vec是从大量文本语料中以无监督的方式学习语义知识的一种模型,它被大量地用在自然语言处理(NLP)中。那么它是如何帮助我们做自然语言处理呢?Word2Vec其实就是通过学习文本来用词向量的方式表征词的语义信息,即通过一个嵌入空间使得语义上相似的单词在该空间内距离很近。

Embedding其实就是一个映射,将单词从原先所属的空间映射到新的多维空间中,也就是把原先词所在空间嵌入到一个新的空间中去。

我们从直观角度上来理解一下,cat这个单词和kitten属于语义上很相近的词,而dog和kitten则不是那么相近,iphone这个单词和kitten的语义就差的更远了。通过对词汇表中单词进行这种数值表示方式的学习(也就是将单词转换为词向量),能够让我们基于这样的数值进行向量化的操作从而得到一些有趣的结论。比如说,如果我们对词向量kitten、cat以及dog执行这样的操作:kitten - cat + dog,那么最终得到的嵌入向量(embedded vector)将与puppy这个词向量十分相近。

这里不再展开讲Word2Vec的各种实现,我们侧重于深度学习常用的embedding去讲解。

其实二者的目标是一样的,都是我们为了学到词的稠密的嵌入表示。只不过学习的方式不一样。

Word2vec是无监督的学习方式,利用上下文环境来学习词的嵌入表示,因此可以学到相关词,但是只能捕捉到局部分布信息。

而在Embedding层中,权重的更新是基于标签的信息进行学习,为了达到较高的监督学习的效果,会将Embedding作为网络的一层,根据target进行学习和调整。比如LSTM中对词向量的微调。

简单来说,Word2vec一般单独提前训练好,而Embedding一般作为模型中的层随着模型一同训练。

One-hot 编码

比如:

我喜欢飞桨,我也喜欢你

是重复的,所以只需要一次记录

   我  喜 欢 飞  桨  , 也 你
我  1  0  0  0  0  0  0  0 
喜  0  1  0  0  0  0  0  0 
欢  0  0  1  0  0  0  0  0 
飞  0  0  0  1  0  0  0  0 
桨  0  0  0  0  1  0  0  0 
,  0  0  0  0  0  1  0  0 
也  0  0  0  0  0  0  1  0  
你  0  0  0  0  0  0  0  1 

则原句子的特征就是:

 1  0  0  0  0  0  0  0 
 0  1  0  0  0  0  0  0 
 0  0  1  0  0  0  0  0 
 0  0  0  1  0  0  0  0 
 0  0  0  0  1  0  0  0 
 0  0  0  0  0  1  0  0 
 0  0  0  0  0  0  1  0  
 0  0  0  0  0  0  0  1 

这个一个好处是特征计算简单,直接将稀疏矩阵对应位置相乘相加即可。

另外他的劣势是由于是稀疏矩阵,大部分信息都是0,浪费存储空间和计算空间,到这就推导出embeddding层的作用了

将其转换为如下的查表的形式,就是embedding的作用了。

在这里插入图片描述

# 来自官方的embedding  Demo

import paddle
import numpy as np

x = np.array([[1, 0, 0],
              [0, 1, 0],
              [0, 0, 1]])

x = paddle.to_tensor(x, stop_gradient=False)

embedding = paddle.nn.Embedding(10, 4, sparse=True)

w0=np.full(shape=(10, 4), fill_value=0).astype(np.float32)
embedding.weight.set_value(w0)

adam = paddle.optimizer.Adam(parameters=[embedding.weight], learning_rate=0.01)
adam.clear_grad()

out=embedding(x)

print(out)

out.backward()
adam.step()

可能这里还是看不懂,那我们以实际的例子为介绍进行展开。

我们随便找一个文章进行处理。

这里我去百度找到了四大名著的中文TXT,就以这个为例进行展示。
(侵删,请勿商用)

玩转四大名著-实例

1 读文本

# 文件路径
def readTxt(path_to_file):
    test_sentence = open(path_to_file, 'rb').read().decode(encoding='UTF-8')

    # 文本长度是指文本中的字符个数
    print ('{} : Length of text: {} characters'.format(path_to_file.split('.')[0] , len(test_sentence)))
    
    return test_sentence
test_sentence = []

hongloumeng =  readTxt('红楼梦.txt')
sanguoyanyi =  readTxt('三国演义.txt')
xiyouji =  readTxt('西游记.txt')
shuihuzhuan =  readTxt('水浒传.txt')

test_sentence = hongloumeng + sanguoyanyi + xiyouji + shuihuzhuan

print(len(test_sentence))
红楼梦 : Length of text: 932224 characters
三国演义 : Length of text: 614770 characters
西游记 : Length of text: 739496 characters
水浒传 : Length of text: 931284 characters
3217774

2 数据预处理

因为标点符号本身无实际意义,用string库中的punctuation,完成英文符号的替换。

from string import punctuation

# str库的punctuation只是英文字符,我们加上中文的字符和字母、空格 因为我们是中文文集
punctuation = punctuation + ' qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM,。、  ;‘’【】』!@#э¥%……&*()'
print(punctuation)

# 特殊符号全部换成空格 后续分句需要使用
process_dicts={i:'' for i in punctuation}
print(process_dicts)

punc_table = str.maketrans(process_dicts)
test_sentence = test_sentence.translate(punc_table)

# 去掉无关的文本以及空格后
print ('Length of text: {} characters'.format(len(test_sentence)))

!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM,。、  ;‘’【】』!@#э¥%……&*()
{'!': '', '"': '', '#': '', '$': '', '%': '', '&': '', "'": '', '(': '', ')': '', '*': '', '+': '', ',': '', '-': '', '.': '', '/': '', ':': '', ';': '', '<': '', '=': '', '>': '', '?': '', '@': '', '[': '', '\\': '', ']': '', '^': '', '_': '', '`': '', '{': '', '|': '', '}': '', '~': '', ' ': '', 'q': '', 'w': '', 'e': '', 'r': '', 't': '', 'y': '', 'u': '', 'i': '', 'o': '', 'p': '', 'a': '', 's': '', 'd': '', 'f': '', 'g': '', 'h': '', 'j': '', 'k': '', 'l': '', 'z': '', 'x': '', 'c': '', 'v': '', 'b': '', 'n': '', 'm': '', 'Q': '', 'W': '', 'E': '', 'R': '', 'T': '', 'Y': '', 'U': '', 'I': '', 'O': '', 'P': '', 'A': '', 'S': '', 'D': '', 'F': '', 'G': '', 'H': '', 'J': '', 'K': '', 'L': '', 'Z': '', 'X': '', 'C': '', 'V': '', 'B': '', 'N': '', 'M': '', ',': '', '。': '', '、': '', ';': '', '‘': '', '’': '', '【': '', '】': '', '』': '', '!': '', 'э': '', '¥': '', '…': '', '(': '', ')': ''}
Length of text: 2733255 characters

由于词表的的长尾,会降低模型训练的速度与精度。

因此取词频前3000的单词作为词表,如果不在词表中的单词都用 ‘’ 替换。

下图来自百度

在这里插入图片描述

与此同时,我们统计一下词频

test_sentence_list = test_sentence.lower().split()

test_sentence_list[0:5]
['红楼梦',
 '第一卷01030章第一回',
 '甄士隐梦幻识通灵贾雨村风尘怀闺秀',
 '此开卷第一回也作者自云因曾历过一番梦幻之后故将真事隐去而借通灵之说撰此一书也故曰甄士隐云云但书中所记何事何人自又云今风尘碌碌一事无成忽念及当日所有之女子一一细考较去觉其行止见识皆出于我之上何我堂堂须眉诚不若彼裙钗哉实愧则有余悔又无益之大无可如何之日也当此则自欲将已往所赖天恩祖德锦衣纨之时饫甘餍肥之日背父兄教育之恩负师友规谈之德以至今日一技无成半生潦倒之罪编述一集以告天下人我之罪固不免然闺阁中本自历历有人万不可因我之不肖自护己短一并使其泯灭也虽今日之茅椽蓬牖瓦灶绳床其晨夕风露阶柳庭花亦未有妨我之襟怀笔墨者虽我未学下笔无文又何妨用假语村言敷演出一段故事来亦可使闺阁昭传复可悦世之目破人愁闷不亦宜乎故曰贾雨村云云',
 '此回中凡用梦用幻等字是提醒阅者眼目亦是此书立意本旨']
word_dict_count = {}
word_dict = []
for word in test_sentence_list:
    for i in word: 
        word_dict_count[i] = word_dict_count.get(i, 0) + 1
        word_dict.append(i)

word_list = []
# 按照值得大小排序
soted_word_list = sorted(word_dict_count.items(), key=lambda x: x[1], reverse=True)
for key in soted_word_list:
    word_list.append(key[0])

word_list = word_list[:3000]
print(len(soted_word_list))
print(soted_word_list[0:10])
print(len(word_list))
print(word_list[0:10])

print(word_dict[0:10])
5916
[('了', 42354), ('不', 39643), ('一', 34742), ('道', 33456), (':', 33285), ('来', 30995), ('“', 30758), ('”', 30619), ('人', 28750), ('的', 26417)]
3000
['了', '不', '一', '道', ':', '来', '“', '”', '人', '的']
['红', '楼', '梦', '第', '一', '卷', '0', '1', '0', '3']

3 模型训练参数设置

# 设置参数
hidden_size = 2048               # Linear层 参数
embedding_dim = 512              # embedding 维度
batch_size = 512                 # batch size 大小
context_size = 2                 # 上下文长度
vocab_size = len(word_list) + 1  # 词表大小
epochs = 10                       # 迭代轮数

4 数据加载

数据格式

将文本拆成了元组的形式,格式为((‘第一个词’, ‘第二个词’), ‘第三个词’)

其中,第三个词就是目标。

trigram = [[[word_dict[i], word_dict[i + 1]], word_dict[i + 2]]
           for i in range(len(word_dict) - 2)]

# 对两千个字进行编号
word_to_idx = {word: i+1 for i, word in enumerate(word_list)}
word_to_idx['<pad>'] = 0
idx_to_word = {word_to_idx[word]: word for word in word_to_idx}

# 看一下数据集
print(trigram[:5])
[[['红', '楼'], '梦'], [['楼', '梦'], '第'], [['梦', '第'], '一'], [['第', '一'], '卷'], [['一', '卷'], '0']]
print(word_to_idx)
{'了': 1, '不': 2, '一': 3, '道': 4, ':': 5, '来': 6, '“': 7, '”': 8, '人': 9, '的': 10, '是': 11, '我': 12, '个': 13, '那': 14, '有': 15, '他': 16, '去': 17, '说': 18, '你': 19, '大': 20, '这': 21, '上': 22, '见': 23, '之': 24, '得': 25, '里': 26, '在': 27, '下': 28, '也': 29, '子': 30, '只': 31, '着': 32, '将': 33, '出': 34, '又': 35, '便': 36, '?': 37, '军': 38, '马': 39, '儿': 40, '曰': 41, '日': 42, '头': 43, '中': 44, '此': 45, '行': 46, '三': 47, '到': 48, '时': 49, '好': 50, '如': 51, '回': 52, '两': 53, '家': 54, '二': 55, '都': 56, '知': 57, '自': 58, '小': 59, '兵': 60, '天': 61, '老': 62, '宝': 63, '看': 64, '与': 65, '们': 66, '前': 67, '玉': 68, '太': 69, '王': 70, '何': 71, '今': 72, '者': 73, '听': 74, '起': 75, '要': 76, '却': 77, '心': 78, '山': 79, '就': 80, '后': 81, '事': 82, '等': 83, '为': 84, '可': 85, '笑': 86, '无': 87, '正': 88, '么': 89, '门': 90, '面': 91, '把': 92, '十': 93, '贾': 94, '叫': 95, '过': 96, '明': 97, '众': 98, '打': 99, '江': 100, '问': 101, '身': 102, '宋': 103, '公': 104, '走': 105, '手': 106, '当': 107, '先': 108, '相': 109, '还': 110, '进': 111, '以': 112, '城': 113, '生': 114, '师': 115, '地': 116, '处': 117, '和': 118, '于': 119, '些': 120, '言': 121, '话': 122, '四': 123, '若': 124, '入': 125, '张': 126, '而': 127, '方': 128, '已': 129, '路': 130,
······ 'X': 3000,  '<pad>': 0

5 dataset类构建

import numpy as np
import paddle
from paddle.io import Dataset,DataLoader

class TrainDataset(Dataset):
    def __init__(self, tuple_data):
        self.tuple_data = tuple_data

    def __getitem__(self, idx):
        data = self.tuple_data[idx][0]
        label = self.tuple_data[idx][1]
        data = np.array(list(map(lambda word: word_to_idx.get(word, 0), data)),dtype=np.int64)
        label = np.array(word_to_idx.get(label, 0), dtype=np.int64)
        return data, label
    
    def __len__(self):
        return len(self.tuple_data)
    
train_dataset = TrainDataset(trigram)

# 加载数据
train_loader = DataLoader(train_dataset, return_list=True, shuffle=True, 
                                    batch_size=batch_size, drop_last=True)
for i, data in enumerate(train_dataset):
    print("data -------------\n",data[0])
    print("label ------------\n",data[1])
    break
data -------------
 [335 566]
label ------------
 889

6 模型组网

为了构建Trigram模型,用一层 Embedding 与两层 Linear 完成构建。Embedding 层对输入的前两个字embedding,然后输入到后面的两个Linear层中,完成特征提取。

import paddle.nn.functional as F

class NGramModel(paddle.nn.Layer):
    def __init__(self, vocab_size=vocab_size, embedding_dim=embedding_dim, context_size=context_size):
        super(NGramModel, self).__init__()
        self.embedding = paddle.nn.Embedding(num_embeddings=vocab_size, embedding_dim=embedding_dim)
        self.linear1 = paddle.nn.Linear(context_size * embedding_dim, hidden_size)
        self.linear2 = paddle.nn.Linear(hidden_size, vocab_size)

    def forward(self, x):
        x = self.embedding(x)
        x = paddle.reshape(x, [-1, context_size * embedding_dim])
        x = self.linear1(x)
        x = F.relu(x)
        x = self.linear2(x)
        x = F.relu(x)
        return x
epochs = 20

n_gram_model = paddle.Model(NGramModel(vocab_size, embedding_dim, context_size)) # 用 Model封装 NGramModel

# 模型配置
n_gram_model.prepare(optimizer=paddle.optimizer.Adam(learning_rate=0.001, 
                     parameters=n_gram_model.parameters()),
                     loss=paddle.nn.CrossEntropyLoss())

# 模型训练
n_gram_model.fit(train_loader, 
                 epochs=epochs,
                 batch_size=batch_size,
                 verbose=1)
The loss value printed in the log is the current step, and the metric is the average value of previous steps.
Epoch 1/20
step 5245/5245 [==============================] - loss: 6.7501 - 9ms/step        
Epoch 2/20
step 5245/5245 [==============================] - loss: 6.6608 - 9ms/step        
Epoch 3/20
step 5245/5245 [==============================] - loss: 6.5338 - 9ms/step         
Epoch 4/20
step 5245/5245 [==============================] - loss: 6.3674 - 9ms/step        
Epoch 5/20
step 5245/5245 [==============================] - loss: 6.4726 - 9ms/step         
Epoch 6/20
step 5245/5245 [==============================] - loss: 6.5758 - 9ms/step        
Epoch 7/20
step 5245/5245 [==============================] - loss: 6.4533 - 9ms/step         
Epoch 8/20
step 5245/5245 [==============================] - loss: 6.5346 - 9ms/step         
Epoch 9/20
step 5245/5245 [==============================] - loss: 6.2345 - 9ms/step         
Epoch 10/20
step 5245/5245 [==============================] - loss: 6.3339 - 9ms/step        
Epoch 11/20
step 5245/5245 [==============================] - loss: 6.4343 - 9ms/step         
Epoch 12/20
step 5245/5245 [==============================] - loss: 6.6187 - 9ms/step         
Epoch 13/20
step 5245/5245 [==============================] - loss: 6.3213 - 9ms/step         
Epoch 14/20
step 5245/5245 [==============================] - loss: 6.4034 - 8ms/step        
Epoch 15/20
step 5245/5245 [==============================] - loss: 6.2653 - 8ms/step        
Epoch 16/20
step 5245/5245 [==============================] - loss: 6.3361 - 8ms/step        
Epoch 17/20
step 5245/5245 [==============================] - loss: 6.3716 - 8ms/step        
Epoch 18/20
step 5245/5245 [==============================] - loss: 6.4298 - 8ms/step        
Epoch 19/20
step 5245/5245 [==============================] - loss: 6.4861 - 9ms/step         
Epoch 20/20
step 5245/5245 [==============================] - loss: 6.3069 - 9ms/step        
import random

def test(model):
    model.eval()
    # 从最后1000组数据中随机选取1个
    idx = random.randint(len(trigram)-1000, len(trigram)-1)
    print('the input words is: ' + trigram[idx][0][0] + ', ' + trigram[idx][0][1])
    x_data = list(map(lambda word: word_to_idx.get(word, 0), trigram[idx][0]))
    x_data = paddle.to_tensor(np.array(x_data))
    predicts = model(x_data)
    predicts = predicts.numpy().tolist()[0]
    predicts = predicts.index(max(predicts))
    print('the predict words is: ' + idx_to_word[predicts])
    y_data = trigram[idx][1]
    print('the true words is: ' + y_data)


cts.index(max(predicts))
    print('the predict words is: ' + idx_to_word[predicts])
    y_data = trigram[idx][1]
    print('the true words is: ' + y_data)


test(model)
the input words is: 州, 蓼
the predict words is:  子
the true words is:  子

总结

这样就完成了,一整套的embedding的学习以及入门级的使用。当然,因为是自己收集的文本不够大,效果不理想也是在情理之中的,一般情况下都会去拿别人已经训练好的预训练词向量模型去跑,或者你的数据集是足够的充足也是可以的。

个人总结

全网同名:

iterhui

我在AI Studio上获得至尊等级,点亮10个徽章,来互关呀~

https://aistudio.baidu.com/aistudio/personalcenter/thirdview/643467

此文仅为搬运,原作链接:https://aistudio.baidu.com/aistudio/projectdetail/4338296

Logo

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

更多推荐