RNN的使用
1. RNN 要解决什么问题?
- 时序信号的抽取特征
- 顺序
2. RNN 的解决方法
- 循环
- 逐个处理
- 前后依赖
- 后一项直接依赖前一项,间接依赖前面所有的项
- 前后进行【手递手】传递
- 中间隐藏状态
3. RNN API 如何调用?
- nn.RNN
- 自动挡(自动循环)
- 用于编码器
- nn.RNNCell
- 手动挡(手动循环)
- 用于解码器
python
import torch
from torch import nn
"""
1. Simple RNN
"""
"""
时序类数据结构:[seq_len, batch_size, embedding_dim]
seq_len:序列长度
batch_size:批量数据有多少个
embedding_dim:嵌入维度,也就是每个词是多大的向量
"""
# 比如一个短信70个字,3个短信,每个字是256个维度
X = torch.randn(70, 3, 256)
h0 = torch.zeros(1, 3, 512, dtype=torch.float32)
X.shape, h0.shape
# 构建一个循环神经网络,输入层维度256,隐藏层维度512
rnn = nn.RNN(input_size=256, hidden_size=512)
# 调用RNN
out, hn = rnn(X, h0)
# [seq_len, batch_size, hidden_size]
out.shape
# [1, batch_size, hidden_size]
hn.shape
4. LSTM 博客
http://colah.github.io/posts/2015-08-Understanding-LSTMs/
python
"""
2、 LSTM 长短期记忆网络
Long 长
Short 短
Term 期
Memory 记忆
"""
"""
导入必要的模块
导入 PyTorch 库及其神经网络模块 nn
"""
import torch
from torch import nn
"""
(1)定义 LSTM 层
"""
# Inputs: input, (h_0, c_0)
# Outputs: output, (h_n, c_n)
# 创建一个 LSTM 层,输入维度为 256,隐藏层维度为 512
lstm = nn.LSTM(input_size=256, hidden_size=512)
"""
(2)准备输入数据和初始状态
"""
# 准备输入数据(70个时间步,每个时间步有3个样本,每个样本的特征维度为256)
X = torch.randn(70, 3, 256)
# 初始化隐藏状态h0和细胞状态c0,1个LSTM层,3个样本,每个样本的隐藏状态维度为512
h0 = torch.zeros(1, 3, 512, dtype=torch.float32)
c0 = torch.zeros(1, 3, 512, dtype=torch.float32)
"""
(3)前向传播
"""
#out是LSTM的输出,hn是最后一个时间步的隐藏状态,cn是最后一个时间步的细胞状态
out, (hn, cn) = lstm(X, (h0, c0))
"""
(4)检查输出的形状
"""
# [70, 3, 512]
out.shape
# [1, 3, 512]
hn.shape
# [1, 3, 512]
cn.shape
"""
(1)定义 LSTMCell 层
"""
# Inputs: input, (h_0, c_0)
# Outputs: (h_1, c_1)
#创建一个 LSTMCell 层,输入维度为 256,隐藏层维度为 512
lstm_cell = nn.LSTMCell(input_size=256, hidden_size=512)
"""
(2)准备输入数据和初始状态
"""
# 准备输入数据
X = torch.randn(70, 3, 256)
# 初始化隐藏状态 h0 和细胞状态 c0
h0 = torch.zeros(3, 512, dtype=torch.float32)
c0 = torch.zeros(3, 512, dtype=torch.float32)
"""
(3)处理第一个时间步的数据
"""
X0 = X[0, :, :]
X0.shape
"""
(4)前向传播
"""
hn, cn = lstm_cell(X0, (h0, c0))
"""
(5)检查输出的形状
"""
hn.shape
cn.shape
X.size(0)
"""
(6)循环处理所有时间步的数据
"""
out = []
for x in X:
h0, c0 = lstm_cell(x, (h0, c0))
out.append(h0)
"""
(7)拼接所有时间步的输出
"""
# 最终所有步的短期状态
out = torch.stack(tensors=out, dim=0)
"""
(8)获取最后一步的状态
"""
hn = h0
cn = c0
"""
总结:
LSTM 层:处理整个序列数据,返回所有时间步的输出和最后一个时间步的隐藏状态及细胞状态。
LSTMCell 层:逐个时间步处理数据,返回每个时间步的隐藏状态和细胞状态。
"""
5. 信息
- CRUD工程师
-
- 增删改查
-
- LSTM
-
- 门控思想
-
- CRUD思想
- (1)遗忘门:先丢掉一部分不重要信息
- (2)输入门:增加重要的信息
-
- RGU
- 核心思想:吃LSTM的红利,化简LSTM
- 调用层面:跟Simple RNN是一样的
所有的网络都是在处理特征,这是人工智能的核心问题,不论是全链接、卷积、循环和transformer,它们都是在处理特征,所以我们只关注特征的维度。
情感识别 sentiment analysis
1. 业务本质:
- 从一段文本中,判断说话人的情感色彩
- 正面:positive
- 负面:negative
- 中立:neutral
2. 技术本质:
- 文本分类
3. 流程:
(1)了解数据
(2)构建词典
分词
jieba
(3)搭建模型
① 向量化
Embedding
② RNN 抽特征
③ Linear 分类
(4)训练模型
(5)评估模型
(6)预测结果
python
"""
需求:酒店评论分析
"""
"""
1. 样本路径和类别读取
"""
"""
训练数据聚合
"""
import os
train_root = os.path.join("hotel", "train")
train_texts = []
train_labels = []
for label in os.listdir(train_root):
label_root = os.path.join(train_root, label)
for file in os.listdir(label_root):
file_path = os.path.join(label_root, file)
# 聚合结果
train_texts.append(file_path)
train_labels.append(label)
# 打印数据
len(train_texts), len(train_labels)
"""
测试数据聚合
"""
test_root = os.path.join("hotel", "test")
test_texts = []
test_labels = []
for label in os.listdir(test_root):
label_root = os.path.join(test_root, label)
for file in os.listdir(label_root):
file_path = os.path.join(label_root, file)
# 聚合结果
test_texts.append(file_path)
test_labels.append(label)
# 打印数据
len(test_texts), len(test_labels)
python
"""
2. 构建分词器
分词,把句子变 token
把所有不同的token聚在一起
做 0 ~ N-1 的编码
"""
SEQ_LEN = 30
import jieba
# pip install opencc -U
import opencc
class Tokenizer(object):
"""
定义一个分词器
"""
def __init__(self, X, y):
"""
训练的语料
"""
self.X = X
self.y = y
self.t2s = opencc.OpenCC(config="t2s")
self._build_dict()
def _build_dict(self):
"""
构建字典
"""
# 1. 获取所有的 token
words = {"<PAD>", "<UNK>"}
for file in self.X:
# 1. 打开文件
with open(file=file, mode="r", encoding="gbk", errors="ignore") as f:
text = f.read().replace("\n", "")
text = self.t2s.convert(text=text)
words.update(set(jieba.lcut(text)))
# 2. 构建文本字典
self.word2idx = {word: idx for idx, word in enumerate(words)}
self.idx2word = {idx: word for word, idx in self.word2idx.items()}
# 3. 删掉 数据集
del self.X
# 4. 构建标签字典
labels = set(train_labels)
self.label2idx = {label: idx for idx, label in enumerate(labels)}
self.idx2label = {idx: label for label, idx in self.label2idx.items()}
# 5. 删除 数据集
del self.y
def encode(self, text, seq_len=SEQ_LEN):
"""
text --> tokens --> ids
自我扩展:
- 右侧截断或填充
- 左边?
- 随机?
"""
# 1. 繁体转简体
text = text.replace("\n", "")
text = self.t2s.convert(text=text)
# 2. 分词
text = jieba.lcut(text)
# 3. 统一长度
text = (text + ["<PAD>"] * seq_len)[:seq_len]
# 4. 转 id
ids = [self.word2idx.get(word, self.word2idx.get("<UNK>")) for word in text]
return ids
def decode(self, ids):
"""
ids --> tokens --> text
"""
text = "".join([self.idx2word.get(_id, "") for _id in ids])
return text
def __str__(self):
"""
输: 分词器基本信息
"""
return f"""
Tokenizer Info:
--> Num of Tokens: {len(self.word2idx)}
--> Num of Labels: {len(self.label2idx)}
"""
def __repr__(self):
return self.__str__()
python
"""
3. 打包数据
"""
from torch.utils.data import Dataset
from torch.utils.data import DataLoader
import torch
class HotelCommentDataset(Dataset):
"""
自定义数据集
"""
def __init__(self, X, y, seq_len=SEQ_LEN):
"""
初始化
"""
self.X = X
self.y = y
self.seq_len = seq_len
def __getitem__(self, idx):
"""
索引操作
返回第idx个样本
"""
# 1. 文本
file = self.X[idx]
with open(file=file, mode="r", encoding="gbk", errors="ignore") as f:
text = f.read()
ids = tokenizer.encode(text=text, seq_len=self.seq_len)
ids = torch.tensor(data=ids, dtype=torch.long)
# 2. 标签
label = self.y[idx]
label = tokenizer.label2idx.get(label)
label = torch.tensor(data=label, dtype=torch.long)
return ids, label
def __len__(self):
return len(self.X)
# 1. 定义一个分词器
tokenizer = Tokenizer(X=train_texts, y=train_labels)
tokenizer
# 打包数据
train_dataset = HotelCommentDataset(X=train_texts, y=train_labels)
train_dataloader = DataLoader(dataset=train_dataset, shuffle=True, batch_size=128)
test_dataset = HotelCommentDataset(X=test_texts, y=test_labels)
test_dataloader = DataLoader(dataset=test_dataset, shuffle=False, batch_size=256)
for X, y in test_dataloader:
print(X.shape)
print(y.shape)
break
问题
- 输入:65个词
- 时序信号
-
输出:2个分类
-
模型:
- 能否用卷积做?
- 能用什么算法模型来做,取决于数据类型。
- 使用卷积如何搭建?
- 一维:[N, C, L]
- 二维:[N, C, H, W]
- 三维:[N, C, H, W, L]
python
"""
4. 搭建模型
"""
import torch
from torch import nn
"""
每句话65个词,分为2类
"""
class TextCNN(nn.Module):
"""
搭建模型
- 卷积
# [N, C, L]
nn.Conv1d()
"""
def __init__(self, dict_len=len(tokenizer.word2idx), embedding_dim = 256):
super().__init__()
# 向量化
self.embed = nn.Embedding(num_embeddings=dict_len,
embedding_dim=embedding_dim,
padding_idx=tokenizer.word2idx.get("<PAD>"))
# 特征抽取
self.feature_extractor = nn.Sequential(
nn.Conv1d(in_channels=256, out_channels=512, kernel_size=3, stride=1, padding=1),
nn.BatchNorm1d(num_features=512),
nn.ReLU(),
nn.MaxPool1d(kernel_size=2, stride=2, padding=0),
nn.Conv1d(in_channels=512, out_channels=1024, kernel_size=3, stride=1, padding=1),
nn.BatchNorm1d(num_features=1024),
nn.ReLU(),
nn.MaxPool1d(kernel_size=2, stride=2, padding=0)
)
# 分类
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Linear(in_features=1024 * (SEQ_LEN // 2 // 2), out_features=128),
nn.ReLU(),
nn.Linear(in_features=128, out_features=2)
)
def forward(self, x):
# 向量化
x = self.embed(x)
x = torch.permute(input=x, dims=(0, 2, 1))
# 抽特征
x = self.feature_extractor(x)
# 做分类
x = self.classifier(x)
return x
class TextRNN(nn.Module):
"""
搭建模型
- 卷积
# [N, C, L]
nn.Conv1d()
"""
def __init__(self, dict_len=len(tokenizer.word2idx), embedding_dim = 256):
super().__init__()
# 向量化
self.embed = nn.Embedding(num_embeddings=dict_len,
embedding_dim=embedding_dim,
padding_idx=tokenizer.word2idx.get("<PAD>"))
# 特征抽取
self.feature_extractor = nn.RNN(input_size=256, hidden_size=512, num_layers=1, bidirectional=False)
# 分类
self.classifier = nn.Sequential(
nn.Linear(in_features=512, out_features=128),
nn.ReLU(),
nn.Linear(in_features=128, out_features=2)
)
def forward(self, x):
# 向量化
x = self.embed(x)
x = torch.permute(input=x, dims=(1, 0, 2))
# 特征抽取
out, hn = self.feature_extractor(x)
# 分类输出
x = self.classifier(out.sum(dim=0))
return x
model= TextRNN()
for X, y in train_dataloader:
y_pred = model(X)
print(y_pred.shape)
break
python
"""
5. 训练模型
"""
# 检测设备
device = "cuda" if torch.cuda.is_available() else "cpu"
# 实例化模型
model = TextRNN().to(device=device)
# 损失函数
loss_fn = nn.CrossEntropyLoss()
# 优化器
optimizer = torch.optim.Adam(params=model.parameters(), lr=1e-3)
# 轮次
epochs = 20
python
"""
6. 评估
"""
import time
def get_acc(dataloader):
model.eval()
accs = []
with torch.no_grad():
for X, y in dataloader:
# 0. 数据搬家
X = X.to(device=device)
y = y.to(device=device)
# 1. 正向传播
y_pred = model(X)
# 2. 计算结果
y_pred = y_pred.argmax(dim=1)
# 3. 计算准确率
acc = (y_pred == y).to(dtype=torch.float32).mean().item()
# 4. 保存结果
accs.append(acc)
final_acc = round(number=sum(accs) / len(accs), ndigits=6)
return final_acc
def train():
train_acc = get_acc(dataloader=train_dataloader)
test_acc = get_acc(dataloader=test_dataloader)
print(f"初始 Train_acc: {train_acc}, Test_acc: {test_acc}")
for epoch in range(epochs):
model.train()
start_time = time.time()
for X, y in train_dataloader:
# 0. 数据搬家
X = X.to(device=device)
y = y.to(device=device)
# 1. 正向传播
y_pred = model(X)
# 2. 计算误差
loss = loss_fn(y_pred, y)
# 3. 反向传播
loss.backward()
# 4. 优化一步
optimizer.step()
# 5. 清空梯度
optimizer.zero_grad()
stop_time = time.time()
# 每轮结束后测试一下
train_acc = get_acc(dataloader=train_dataloader)
test_acc = get_acc(dataloader=test_dataloader)
print(f"Epoch: {epoch + 1}, Train_acc: {train_acc}, Test_acc: {test_acc}, Train_time: {stop_time-start_time}")
train()