目录
摘要
本篇文章继续学习尚硅谷深度学习教程,学习内容是RNN相关概念,及相关API使用方法
1.自然语言处理
我们平日使用的语言,如汉语或英语,称为自然语言。自然语言处理(Natural Language Processing,NLP)的目标就是让计算机理解人类语言,进而完成对我们有帮助的事情。说到计算机可以理解的语言,我们可能会想到编程语言或者标记语言等。这些语言的语法定义可以唯一性地解释代码含义,计算机也能根据确定的规则分析代码。编程语言是一种机械的、缺乏活力的语言。换句话说,它是一种"硬语言"。而汉语或英语等自然语言是"软语言",其含义和形式会灵活变化,并且会不断出现新的词语或新的含义。要让计算机去理解自然语言,使用常规方法是无法办到的。
基于同义词词典的方法
具有相同(同义词)或类似(近义词)含义的单词,可以归到同一个类别中;而根据单词"整体-部分"或者"上位-下位"关系,可以构建出层级的树状图。
这样,就可以构成一个庞大的"单词网络",用它就可以教会计算机单词之间的关系,从而计算出单词的"相似度"。
主要缺点:
- 需要人工逐个定义单词之间的相关性,非常费时费力;
- 新词不断出现,语言不断变化,词典维护成本极高;
- 在表现力上也有限制。
基于计数的方法
大量的文本数据,构成了 语料库(corpus)。
我们的目的,就是从语料库中,自动且高效地提取出语言的"本质"。最简单的做法,就是统计"词频"。
- 分词:对词进行统计,首先需要对文本内容进行切分,找出一个个基本单元;
- 词关联ID:给单词标上一个 ID,构建单词和ID的关联字典(称为"词表");
- 词向量化:用一个固定长度的向量来表示单词,也称为词的"分布式表示"。
对每一个词,可以统计它周围出现了什么单词、出现了多少次(称为"上下文");把这些词频统计出来,就构成了一个向量;这个向量就可以表示当前的词了,称为"词向量"(word vector)。这样,所有词对应的向量,汇总起来就是一个矩阵,被称为 共现矩阵(co-occurrence matrix)。
主要缺点:对所有词进行向量化表示的计算复杂度极高。
基于推理的方法
除了基于计数的方法,还可以使用推理的方法把词用向量表示出来。
我们希望在已知上下文的前提下,"推测"当前位置的词是什么。
利用神经网络,接收上下文信息作为输入,通过模型计算,输出各个单词可能得出现概率;从而就可以根据上下文,预测该出现的单词了。
2.词嵌入层
自然语言是由文字构成的,而语言的含义是由单词构成的。即单词是含义的最小单位。因此为了让计算机理解自然语言,首先要让它理解单词含义。
词向量是用于表示单词意义的向量,也可以看作词的特征向量。将词映射到向量的技术称为 词嵌入(Word Embedding)。

还有一种使用向量表示单词意义的方式是独热向量,独热向量很容易构建,但它们通常不是一个好的选择。一个主要原因是独热向量不能准确表达不同词之间的相似度。比如使用余弦相似度
来表示两个词之间的相似程度,由于任意两个不同词的独热向量之间的余弦相似度为0,所以独热向量不能编码词之间的相似性。另一个原因是随着词汇量的增大,独热向量表示的向量大小也会增大,在词汇量较大的情况下会消耗大量的存储资源与计算资源。
将词转换为词向量时:
- 首先需要对文本进行分词,再根据需要进行清洗和标准化。
- 构建词表(Vocabulary),每个词对应一个索引。
- 使用词嵌入矩阵将词索引转换为词向量。
API使用
可使用torch.nn.Embedding来初始化词嵌入矩阵:
python
torch.nn.Embedding(num_embeddings, embedding_dim)
# num_embeddings:词的数量
# embedding_dim:词向量的维度
例:
先安装jieba库用于分词:pip install jieba。
python
mport torch
import torch.nn as nn
import jieba
# 设置随机种子
torch.manual_seed(42)
text = "自然语言是由文字构成的,而语言的含义是由单词构成的。即单词是含义的最小单位。因此为了让计算机理解自然语言,首先要让它理解单词含义。"
# 自定义停用词和标点符号
stopwords = {"的", "是", "而", "由", ",", "。", "、"}
# 分词,过滤停用词和标点,去重,构建词表
words = [word for word in jieba.lcut(text) if word not in stopwords]
vocab = list(set(words)) # 词表
# 构建词到索引的映射
word2idx = dict()
for idx, word in enumerate(vocab):
word2idx[word] = idx
# 初始化嵌入层
embed = nn.Embedding(num_embeddings=len(word2idx), embedding_dim=5)
# 打印词向量
for idx, word in enumerate(vocab):
word_vec = embed(torch.tensor(idx)) # 通过索引获取词向量
print(f"{idx:>2}:{word:8}\t{word_vec.detach().numpy()}")

3.循环网络层
文本是连续的,具有序列特性。如果其序列被重排可能就会失去原有的意义。比如"狗咬人"这段文本具有序列关系,如果文字的序列颠倒可能就会表达不同的意思。
目前我们接触的神经网络都是前馈型神经网络。前馈(feedforward)是指网络的传播方向是单向的。具体地说,将输入信号传给下一层,下一层接收到信号后传给下下一层,然后再传给下下下一层...像这样,信号仅在一个方向上传播。虽然前馈网络结构简单、易于理解,并且可以应用于许多任务中。不过,这种网络存在一个大问题,就是不能很好地处理时间序列数据。更确切地说,单纯的前馈网络无法充分学习时序数据的性质。于是,循环神经网络(Recurrent Neural Network,RNN)应运而生。
RNN层具有环路,通过环路数据可以在层内循环。向时序数据输入层中,相应的会输出
。

RNN层的循环的展开

由图可知,各个时刻的RNN层接收传给该层的输入和前一个时刻RNN层的输出
,据此计算当前时刻RNN层的输出
:

RNN层有2个权重,分别是与输入运算的权重
,和与前一时刻RNN层的输出
(也叫隐藏状态,隐状态)运算的权重。执行完乘积和求和运算之后使用tanh函数转换,其结果就是时刻t的输出
。
API使用
可使用torch.nn.RNN来初始化RNN层:
python
rnn = torch.nn.RNN(input_size, hidden_size, num_layers)
# input_size:输入数据的特征数量
# hidden_size:隐藏状态的特征数量
# num_layers:隐藏层的层数,如果设置多个层,前一个隐藏层的输出作为下一个隐藏层的输入
调用时需要传入2个参数:
python
output, hn = rnn(input, hx)
# input:输入数据[seq_len序列长度, batch_size批量大小, input_size]
# hx:初始隐状态[num_layers, batch_size, hidden_size]
# output:输出数据[seq_len, batch_size, hidden_size]
# hn:隐状态[num_layers, batch_size, hidden_size]
例:
python
import torch
rnn = torch.nn.RNN(input_size=8, hidden_size=16, num_layers=2)
input = torch.rand(1, 3, 8)
hx = torch.randn(2, 3, 16)
output, hn = rnn(input=input, hx=hx)
print(output.shape) # torch.Size([1, 3, 16])
print(hn.shape) # torch.Size([2, 3, 16])
4.代码案例:古诗生成
数据预处理

数据在.txt文件中,每行为一首诗。
首先将每个字作为一个词构建词表。并将原文转换为索引序列。
python
import re
import torch
from torch import nn, optim
from torch.utils.data import Dataset, DataLoader
# 数据预处理
def process_poems(file_path):
poems = [] # 保存处理后的诗
char_set = set() # 保存所有不重复的字
with open(file_path, "r", encoding="utf-8") as f:
for line in f:
# 逐行处理
line = re.sub(r"[,。、?!:]", "", line).strip() # 去掉标点符号与两侧空白
# 按字分割并去重
char_set.update(list(line))
# 按字保存诗
poems.append(list(line))
# 构建词表
vocab = list(char_set) + ["<UNK>"]
# 创建词到索引的映射
word2idx = {word: idx for idx, word in enumerate(vocab)}
# 将诗转换为索引序列
sequences = []
for poem in poems:
seq = [word2idx.get(word) for word in poem]
sequences.append(seq)
return sequences, word2idx, vocab
sequences, word2idx, vocab = process_poems("data/poems.txt")
自定义Dataset
构建训练模型的数据集,将一个序列作为输入,另一个序列作为目标。

python
# 自定义Dataset
class PoetryDataset(Dataset):
def __init__(self, sequences, seq_len):
self.seq_len = seq_len
self.data = []
for seq in sequences:
for i in range(0, len(seq) - self.seq_len):
self.data.append((seq[i : i + self.seq_len], seq[i + 1 : i + 1 + self.seq_len]))
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
x = torch.LongTensor(self.data[idx][0])
y = torch.LongTensor(self.data[idx][1])
return x, y
dataset = PoetryDataset(sequences, 24)
搭建模型
词嵌入层→RNN→全连接层
python
# 搭建模型
class PoetryRNN(nn.Module):
def __init__(self, vocab_size, embedding_dim=128, hidden_size=256, num_layers=1):
super().__init__()
self.embed = nn.Embedding(num_embeddings=vocab_size, embedding_dim=embedding_dim)
self.rnn = nn.RNN(input_size=embedding_dim, hidden_size=hidden_size, num_layers=num_layers)
self.linear = nn.Linear(in_features=hidden_size, out_features=vocab_size)
def forward(self, input, hx=None):
embed = self.embed(input)
output, hidden = self.rnn(embed, hx)
output = self.linear(output)
return output, hidden
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = PoetryRNN(len(vocab), 256, 512, 2).to(device)
模型训练
使用交叉熵损失函数,Adam优化方法
python
# 模型训练
def train(model, dataset, lr, epoch_num, batch_size, device):
model.train() # 设置为训练模式
dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
loss = nn.CrossEntropyLoss() # 损失函数
optimizer = optim.Adam(model.parameters(), lr=lr) # 优化器
for epoch in range(epoch_num):
loss_accumulate = 0 # 累加损失
for batch_count, (x, y) in enumerate(dataloader):
# 前向传播
x, y = x.to(device), y.to(device)
output, _ = model(x)
# 反向传播
loss_value = loss(output.transpose(1, 2), y)
optimizer.zero_grad()
loss_value.backward()
optimizer.step()
# 累加损失
loss_accumulate += loss_value.item()
print(f"\repoch:{epoch:0>2}[{'='*(int((batch_count+1) / len(dataloader) * 50)):<50}]", end="")
print(f" loss:{loss_accumulate/len(dataloader):.6f}")
train(model=model, dataset=dataset, lr=1e-3, epoch_num=20, batch_size=32, device=device)
测试
输入一个起始词让模型开始生成
python
# 生成
def generate_poem(model, word2idx, vocab, start_token, line_num=4, line_length=7):
model.eval() # 设置为预测模式
poem = [] # 记录生成结果
current_line_length = line_length # 当前句的剩余长度
start_token = word2idx.get(start_token, word2idx["<UNK>"]) # 起始token
# 如果起始token在词典中,添加到结果中
if start_token != word2idx["<UNK>"]:
poem.append(vocab[start_token])
current_line_length -= 1
input = torch.LongTensor([[start_token]]).to(device) # 输入
hidden = None # 初始化隐状态
with torch.no_grad(): # 关闭梯度计算
for _ in range(line_num): # 生成的行数
for interpunction in [",", "。\n"]: # 每行两句
while current_line_length > 0: # 每句诗line_length个字
output, hidden = model(input, hidden)
prob = torch.softmax(output[0, 0], dim=-1) # 计算概率
next_token = torch.multinomial(prob, 1) # 从概率分布中随机采样
poem.append(vocab[next_token.item()]) # 将采样结果添加到结果中
input = next_token.unsqueeze(0)
current_line_length -= 1
current_line_length = line_length
poem.append(interpunction) # 每句结尾添加标点符号
return "".join(poem) # 将列表转换为字符串
print(generate_poem(model, word2idx, vocab, start_token="一", line_num=4, line_length=7))