使用循环神经网络学习汉语拼音拼写:数据准备和模型实现
使用循环神经网络学习汉语拼音拼写:数据准备和模型实现
本实验使用循环神经网络学习汉语拼音的拼写,重点介绍数据准备和模型实现。
数据准备
- 数据集:拼音数据(无声调),下载地址:https://www.jianguoyun.com/p/DQ3els0Q-rqYBhi3pIgFIAA
- 模型:采用字符模型,每个字符为一个样本,并使用one-hot编码。
- 数据处理:样本是时间相关的,分别实现序列的随机采样和序列的顺序划分。
- 标签:标签Y与X同形状,但时间超前1。
- 数据形状:一次梯度更新使用的数据形状为:(时间步,Batch,类别数)。
模型实现
- 循环单元:使用nn.RNN或GRU作为循环单元。
- 输出层:使用RNN所有时间步的输出。
- 隐状态初始值:0。
训练
- 损失函数:平均交叉熵。
- 数据准备方式:随机采样或顺序划分。
- 梯度截断:如果采用顺序划分,需进行梯度截断。
预测
- 给定一个前缀,进行单步预测和K步预测。
- 预测方式:使用贪心策略或者采样的方式。
Python实现
数据准备
首先,我们需要下载拼音数据集,然后将其转换为模型所需的格式。每个样本都是一个字符,我们可以使用one-hot编码来表示每个字符。在这个任务中,我们使用的数据集不带声调。
import numpy as np
def load_data(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
data = []
for line in lines:
line = line.strip()
if line:
data.append(line)
# 构建字符索引
chars = set(''.join(data))
char2idx = {char: idx for idx, char in enumerate(chars)}
idx2char = {idx: char for char, idx in char2idx.items()}
# 将数据转换为one-hot编码
max_len = max(len(line) for line in data)
X = np.zeros((len(data), max_len, len(chars)), dtype=np.float32)
Y = np.zeros((len(data), max_len, len(chars)), dtype=np.float32)
for i, line in enumerate(data):
for j, char in enumerate(line):
X[i, j, char2idx[char]] = 1.0
if j < len(line) - 1:
Y[i, j, char2idx[line[j+1]]] = 1.0
return X, Y, char2idx, idx2char
X, Y, char2idx, idx2char = load_data('pinyin.txt')
print(X.shape, Y.shape) # (样本数, 时间步, 类别数)
print(char2idx) # 字符索引
print(idx2char) # 索引字符
模型实现
接下来,我们可以使用PyTorch来实现基本的循环神经网络模型。我们可以使用nn.RNN或nn.GRU作为循环单元。输出层的全连接使用RNN所有时间步的输出。隐状态初始值为0。
模型结构如下所示:
class RNNModel(nn.Module):
def __init__(self, input_size, hidden_size, output_size, num_layers=1):
super(RNNModel, self).__init__()
self.hidden_size = hidden_size
self.num_layers = num_layers
self.rnn = nn.GRU(input_size, hidden_size, num_layers, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x, h0=None):
# x: (batch_size, seq_len, input_size)
# h0: (num_layers * num_directions, batch_size, hidden_size)
if h0 is None:
h0 = x.new_zeros(self.num_layers, x.size(0), self.hidden_size)
out, hn = self.rnn(x, h0)
out = self.fc(out)
# out: (batch_size, seq_len, output_size)
# hn: (num_layers * num_directions, batch_size, hidden_size)
return out, hn
训练模型
我们可以使用交叉熵作为损失函数来训练模型。在训练过程中,我们可以使用随机采样或顺序划分的方式来准备数据。如果采用顺序划分的方式,需要进行梯度截断,以避免梯度消失或爆炸。
以下是训练模型的代码:
def train(model, X, Y, lr, batch_size, num_epochs, device='cpu'):
model.train()
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
for epoch in range(num_epochs):
total_loss = 0.0
n_batches = 0
hidden = None
# 随机采样
indices = np.random.permutation(len(X))
for i in range(0, len(X), batch_size):
batch_indices = indices[i:i+batch_size]
batch_X = torch.tensor(X[batch_indices], dtype=torch.float32, device=device)
batch_Y = torch.tensor(Y[batch_indices], dtype=torch.long, device=device)
optimizer.zero_grad()
output, hidden = model(batch_X, hidden)
loss = 0.0
for j in range(batch_X.size(1)):
loss += criterion(output[:, j, :], batch_Y[:, j])
loss /= batch_X.size(1)
loss.backward()
optimizer.step()
total_loss += loss.item()
n_batches += 1
print(f'Epoch {epoch}, loss={total_loss/n_batches:.4f}')
return model
# 使用随机采样训练模型
model = RNNModel(input_size=len(char2idx), hidden_size=128, output_size=len(char2idx))
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model.to(device)
X = X.astype(np.float32)
Y = np.argmax(Y, axis=-1)
Y = Y.astype(np.long)
X = torch.tensor(X, dtype=torch.float32, device=device)
Y = torch.tensor(Y, dtype=torch.long, device=device)
train(model, X, Y, lr=0.001, batch_size=32, num_epochs=10, device=device)
# 使用顺序划分训练模型
model = RNNModel(input_size=len(char2idx), hidden_size=128, output_size=len(char2idx))
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model.to(device)
X = X.astype(np.float32)
Y = np.argmax(Y, axis=-1)
Y = Y.astype(np.long)
X = torch.tensor(X, dtype=torch.float32, device=device)
Y = torch.tensor(Y, dtype=torch.long, device=device)
seq_len = X.size(1)
for epoch in range(10):
total_loss = 0.0
n_batches = 0
hidden = None
# 顺序划分
indices = np.random.permutation(len(X))
for i in range(0, len(X), seq_len):
batch_indices = indices[i:i+seq_len]
if len(batch_indices) < seq_len:
continue
batch_X = X[batch_indices]
batch_Y = Y[batch_indices]
optimizer.zero_grad()
output, hidden = model(batch_X, hidden)
loss = criterion(output.view(-1, output.size(-1)), batch_Y.view(-1))
loss.backward()
nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
total_loss += loss.item()
n_batches += 1
print(f'Epoch {epoch}, loss={total_loss/n_batches:.4f}')
单步预测和K步预测
最后,我们可以使用模型进行单步预测和K步预测。给定一个前缀,我们可以使用模型预测下一个字符。预测的方式可以使用贪心策略或者采样的方式。对于K步预测,我们可以使用循环预测的方式,即将前一个字符作为下一个字符的前缀进行预测,直到预测出K个字符为止。
以下是单步预测和K步预测的代码:
def predict(model, prefix, char2idx, idx2char, max_len=20, strategy='greedy', k=5, device='cpu'):
model.eval()
prefix = list(prefix)
input = torch.zeros((1, max_len, len(char2idx)), dtype=torch.float32, device=device)
for i, char in enumerate(prefix):
input[0, i, char2idx[char]] = 1.0
output, hidden = model(input)
probs = torch.softmax(output[:, -1, :], dim=-1).squeeze().cpu().detach().numpy()
if strategy == 'greedy':
idx = np.argmax(probs)
result = idx2char[idx]
else:
indices = np.argsort(-probs)[:k]
probs = probs[indices]
probs /= np.sum(probs)
idx = np.random.choice(indices, p=probs)
result = idx2char[idx]
prefix.append(result)
for i in range(max_len - len(prefix)):
input.fill_(0.0)
for j, char in enumerate(prefix):
input[0, j, char2idx[char]] = 1.0
output, hidden = model(input, hidden)
probs = torch.softmax(output[:, -1, :], dim=-1).squeeze().cpu().detach().numpy()
if strategy == 'greedy':
idx = np.argmax(probs)
result = idx2char[idx]
else:
indices = np.argsort(-probs)[:k]
probs = probs[indices]
probs /= np.sum(probs)
idx = np.random.choice(indices, p=probs)
result = idx2char[idx]
prefix.append(result)
if result == '<EOS>':
break
return ''.join(prefix)
# 单步预测
model.eval()
prefix = 'ni'
result = predict(model, prefix, char2idx, idx2char, max_len=20, strategy='greedy', k=5, device=device)
print(result)
# K步预测
model.eval()
prefix = 'ni'
result = predict(model, prefix, char2idx, idx2char, max_len=20, strategy='sampling', k=5, device=device)
print(result)
原文地址: https://www.cveoy.top/t/topic/onrh 著作权归作者所有。请勿转载和采集!