使用循环神经网络学习汉语拼音拼写:数据准备和模型实现

本实验使用循环神经网络学习汉语拼音的拼写,重点介绍数据准备和模型实现。

数据准备

  • 数据集:拼音数据(无声调),下载地址: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 著作权归作者所有。请勿转载和采集!

免费AI点我,无需注册和登录