循环神经网络
2026/9/11 17:28:45 网站建设 项目流程

一、自然语言处理概述

我们平日使用的语言,如汉语或英语,称为自然语言。自然语言处理(Natural Language Processing,NLP)的目标就是让计算机理解人类语言,进而完成对我们有帮助的事情。

说到计算机可以理解的语言,我们可能会想到编程语言或者标记语言等。这些语言的语法定义可以唯一性地解释代码含义,计算机也能根据确定的规则分析代码。编程语言是一种机械的、缺乏活力的语言。换句话说,它是一种“硬语言”。而汉语或英语等自然语言是“软语言”,其含义和形式会灵活变化,并且会不断出现新的词语或新的含义。要让计算机去理解自然语言,使用常规方法是无法办到的。

1.1、基于同义词词典的方法

具有相同(同义词)或类似(近义词)含义的单词,可以归到同一个类别中;而根据单词“整体-部分”或者“上位-下位”关系,可以构建出层级的树状图。

这样,就可以构成一个庞大的“单词网络”,用它就可以教会计算机单词之间的关系,从而计算出单词的“相似度”。

1.2、基于计数的方法

大量的文本数据,构成了语料库(corpus)。我们的目的,就是从语料库中,自动且高效地提取出语言的“本质”。最简单的做法,就是统计“词频”。

1、分词:对词进行统计,首先需要对文本内容进行切分,找出一个个基本单元,也就是一个token;

2、词关联ID:给单词标上一个 ID,构建单词和ID的关联字典(称为“词表”);

3、词向量化:用一个固定长度的向量来表示单词,也称为词的“分布式表示”

对每一个词,可以统计它周围出现了什么单词、出现了多少次(称为“上下文”);把这些词频统计出来,就构成了一个向量;这个向量就可以表示当前的词了,称为“词向量”(word vector)。这样,所有词对应的向量,汇总起来就是一个矩阵,被称为共现矩阵(co-occurrence matrix)。

1.3、基于推理的方法

除了基于计数的方法,还可以使用推理的方法把词用向量表示出来。

我们希望在已知上下文的前提下,“推测”当前位置的词是什么。利用神经网络,接收上下文信息作为输入,通过模型计算,输出各个单词可能得出现概率;从而就可以根据上下文,预测该出现的单词了。

二、词嵌入层

2.1、什么是词嵌入

是用来回去上述所说的词向量的技术,自然语言是由文字构成的,而语言的含义是由单词构成的。即单词是含义的最小单位。因此为了让计算机理解自然语言,首先要让它理解单词含义。

词向量是用于表示单词意义的向量,也可以看作词的特征向量。将词映射到向量的技术称为词嵌入(Word Embedding)。

还有一种使用向量表示单词意义的方式是独热向量,独热向量很容易构建,但它们通常不是一个好的选择。一个主要原因是独热向量不能准确表达不同词之间的相似度,因为任意两个不同词的独热向量之间的余弦相似度为0,所以独热向量不能编码词之间的相似性。另一个原因是随着词汇量的增大,独热向量表示的向量大小也会增大,在词汇量较大的情况下会消耗大量的存储资源与计算资源。

首先需要对文本进行分词,再根据需要进行清洗和标准化。

构建词表(Vocabulary),每个词对应一个索引。就是把id换成独热编码的表示。

使用词嵌入矩阵将词索引转换为词向量。

其实本质上就是用神经网络训练出中间的参数矩阵来当作每一个词的词向量编码,但是在pytorch中已经创建好了一个类就叫做Embedding,就不用我们自己搭建神经网络来训练参数矩阵了。我们直接输入一个id,也就是一个独热编码,pytorch中的Embedding层就会自动返回输出这个id对应的抽取出来的词向量。

2.2、API的使用

import 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] print(words) vocab = list(set(words)) # id2word词表,set去重 print(vocab) # 构建词到索引的映射 word2idx = dict() for idx, word in enumerate(vocab): print(idx, word) 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.1、RNN介绍

文本是连续的,具有序列特性。如果其序列被重排可能就会失去原有的意义。比如“狗咬人”这段文本具有序列关系,如果文字的序列颠倒可能就会表达不同的意思。

目前我们接触的神经网络都是前馈型神经网络。前馈(feedforward)是指网络的传播方向是单向的。具体地说,将输入信号传给下一层,下一层接收到信号后传给下下一层,然后再传给下下下一层…像这样,信号仅在一个方向上传播。虽然前馈网络结构简单、易于理解,并且可以应用于许多任务中。不过,这种网络存在一个大问题,就是不能很好地处理时间序列数据。更确切地说,单纯的前馈网络无法充分学习时序数据的性质。于是,循环神经网络(Recurrent Neural Network,RNN)应运而生。

RNN层的循环的展开(这个意思是一个RNN层经过时间反复的输入输出,并不是四个RNN层):

3.2、API的使用

先理解概念,上面这张图代表了两层RNN层,也就是有两个隐藏层。每一层图中虽然画了 4 个 RNN 方块,但它们是同一个层不同时刻展开表示,不是 4 个不同的RNN层。

初始化

可使用torch.nn.RNN来初始化RNN层:

rnn = torch.nn.RNN(input_size, hidden_size, num_layers)

input_size用来表示输入数据的特征维度。就比如词汇表中有10个词,转换成one-hot编码之后就是十维度的特征。

hidden_size代表了每个时间经过RNN的输出后的隐藏特征维度。

num_layers代表模型在垂直层面的深度,一个RNN会经过多轮的反复输入输出,再传给下一个RNN,num_layers就代表了有几个RNN层。

调用

调用时需要传入2个参数:

output, hn = rnn(input, hx)
输入数据:

input: 输入数据[seq_len序列长度, batch_size批量大小, input_size]

举例解释:同时处理3 句话,每句话有4 个单词,每个单词用5 维向量表示。

句子1: "I love you" → 3个词,但为了对齐,补齐到4个(用0填充)
句子2: "Hello world" → 2个词,补齐到4个
句子3: "Good morning" → 2个词,补齐到4个

  • seq_len(序列长度)= 4(每句话有4个词)

  • batch_size(批次大小)= 3(同时处理3句话)

  • input_size(特征维度)= 5(每个词用5维向量表示)

hx: 初始隐状态[num_layers, batch_size, hidden_size]

  • num_layers= 2(2层RNN堆叠)

  • batch_size= 3(3个句子)

  • hidden_size= 5(隐藏状态维度)

输出数据:

output: 输出数据[seq_len, batch_size, hidden_size]

output就是hn里面组最后一个隐藏层的输出,并且输出数据应该和输入数据的维度是一样的

hn: 隐状态[num_layers, batch_size, hidden_size]

hx = 进入RNN前的初始记忆(起点)

hn = 离开RNN后的最终记忆(终点)

所以hx和hn的维度应该是相同的

四、案例:古诗生成

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 # sequence是所有古诗转换成的汉字id,是一个列表 # word2idx里存储的是字典('床': 0, # '床'这个字符对应数字0) # vocab存的是所有词的列表 sequences, word2idx, vocab = process_poems("D:/BaiduNetdiskDownload/poems.txt") print(len(sequences)) print(len(word2idx)) print(len(vocab)) # 自定义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) print(len(dataset.data)) # 搭建模型 class PoetryRNN(nn.Module): def __init__(self, vocab_size, embedding_dim=128, hidden_size=256, num_layers=1): super().__init__() # vocab_size个词需要嵌入,每个词转换成embedding_dim维度的向量 self.embed = nn.Embedding(num_embeddings=vocab_size, embedding_dim=embedding_dim) # 输入embedding_dim维度的特征,hidden_size隐藏层的特征维度,num_layers代表模型的深度 self.rnn = nn.RNN(input_size=embedding_dim, hidden_size=hidden_size, num_layers=num_layers) # hidden_size代表传入特征维度,输出vocab_size维度特征,有多少个词就输出多少维度 self.linear = nn.Linear(in_features=hidden_size, out_features=vocab_size) def forward(self, input, hx=None): embed = self.embed(input) output, hn = self.rnn(embed, hx) output = self.linear(output) return output, hn device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = PoetryRNN(len(vocab), 256, 512, 2).to(device) # 模型训练 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 # 累加损失 # enumerate 是Python内置函数,用于给迭代对象添加计数器 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) # 生成 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) # 输入 hx = None # 初始化隐状态 with torch.no_grad(): # 关闭梯度计算 for _ in range(line_num): # 生成的行数 for punctuation in [",", "。\n"]: # 每行两句 while current_line_length > 0: # 每句诗line_length个字 output, hn = model(input, hx) 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(punctuation) # 每句结尾添加标点符号 return "".join(poem) # 将列表转换为字符串 print(generate_poem(model, word2idx, vocab, start_token="一", line_num=4, line_length=7))

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询