8.4 基于循环神经网络(RNN)的推荐模型
循环神经网络(Recurrent Neural Network,RNN)是一类以序列(sequence)数据为输入,在序列的演进方向进行递归(recursion)且所有节点(循环单元)按链式连接的递归神经网络(Recursive Neural Network)。基于循环神经网络(RNN)的推荐模型可以使用序列数据的上下文信息来进行推荐。
8.4.1 序列数据的建模
基于循环神经网络(RNN)的推荐模型适用于序列数据的建模,其中推荐是基于用户的历史行为或物品的历史信息。RNN模型能够捕捉序列数据中的时序关系,因此对于推荐系统来说,可以使用RNN来利用用户的历史行为序列或物品的历史信息序列进行推荐。
在推荐系统中,可以将序列数据分为如下两类:
- 用户行为序列:在这种情况下,模型会基于用户的历史行为序列来预测下一个可能的行为。例如,根据用户过去的购买记录,预测用户下一个可能购买的物品。在这种情况下,RNN可以用来建模用户行为的时序关系,以及用户行为之间的依赖关系。
- 物品信息序列:在这种情况下,模型会基于物品的历史信息序列来预测用户的兴趣。例如,根据电影的过去评分和评论信息,预测用户对新电影的评分。在这种情况下,RNN可以用来捕捉物品信息之间的时序关系,以及物品之间的相似性或关联性。
在建模序列数据时,可以使用不同类型的RNN单元,如简单RNN、长短时记忆(LSTM)和门控循环单元(GRU)。这些RNN单元都能够处理序列数据,并具有记忆能力,可以捕捉长期依赖关系。
请看下面的实例文件shashibiya.py,功能是在PyTorch程序中使用循环神经网络生成文本,该模型将训练一个基于莎士比亚作品的语料库生成新的莎士比亚风格的文本。文件shashibiya.py的具体实现流程如下所示。
源码路径:daima\5\xun.py
(1)定义一个文本语料库,即原始文本数据。对应的实现代码如下所示。
corpus = """
From fairest creatures we desire increase,
That thereby beauty's rose might never die,
But as the riper should by time decease,
His tender heir might bear his memory:
But thou contracted to thine own bright eyes,
Feed'st thy light's flame with self-substantial fuel,
Making a famine where abundance lies,
Thy self thy foe, to thy sweet self too cruel:
"""
(2)创建字符级语料库
将文本中的字符进行唯一化,并为每个字符分配一个索引,以便在训练时能够使用整数表示字符。同时,创建字符到索引和索引到字符的映射关系,以便后续的文本生成。num_chars 表示唯一字符的数量。对应的实现代码如下所示。
chars = list(set(corpus))
char_to_idx = {ch: i for i, ch in enumerate(chars)}
idx_to_char = {i: ch for i, ch in enumerate(chars)}
num_chars = len(chars)
(3)将文本拆分为训练样本
将原始文本拆分为输入序列(dataX)和目标序列(dataY),用于训练模型。每个输入序列包含前 seq_length 个字符,而相应的目标序列则是输入序列之后的下一个字符。对应的实现代码如下所示。
seq_length = 100
dataX = []
dataY = []
for i in range(0, len(corpus) - seq_length, 1):
seq_in = corpus[i:i + seq_length]
seq_out = corpus[i + seq_length]
dataX.append([char_to_idx[ch] for ch in seq_in])
dataY.append(char_to_idx[seq_out])
(4)将训练数据转换为Tensor
将输入序列(dataX)和目标序列(dataY)转换为PyTorch张量,以便在模型中使用。对应的实现代码如下所示。
dataX = torch.tensor(dataX, dtype=torch.long)
dataY = torch.tensor(dataY, dtype=torch.long)
(5)定义循环神经网络模型
定义一个循环神经网络(RNN)模型,其中包含一个嵌入层(embedding),一个LSTM层(lstm)和一个全连接层(fc)。forward 方法定义了模型的前向传播逻辑,init_hidden 方法用于初始化隐藏状态。对应的实现代码如下所示。
class RNNModel(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(RNNModel, self).__init__()
self.hidden_size = hidden_size
self.embedding = nn.Embedding(input_size, hidden_size)
self.lstm = nn.LSTM(hidden_size, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x, hidden):
embedded = self.embedding(x)
output, hidden = self.lstm(embedded, hidden)
output = self.fc(output[:, -1, :])
return output, hidden
def init_hidden(self, batch_size):
return (torch.zeros(1, batch_size, self.hidden_size),
torch.zeros(1, batch_size, self.hidden_size))
(6)定义超参数
定义模型的输入大小(input_size)、隐藏层大小(hidden_size)、输出大小(output_size)以及训练的迭代次数(num_epochs)和批次大小(batch_size)。对应的实现代码如下所示。
input_size = num_chars
hidden_size = 128
output_size = num_chars
num_epochs = 200
batch_size = 1
(7)创建数据加载器
使用PyTorch的TensorDataset和DataLoader创建数据加载器,用于批量加载训练数据。对应的实现代码如下所示。
dataset = torch.utils.data.TensorDataset(dataX, dataY)
data_loader = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=True)
(8)实例化模型
根据定义的RNN模型类实例化模型,对应的实现代码如下所示。
model = RNNModel(input_size, hidden_size, output_size)
(9)定义损失函数和优化器
定义交叉熵损失函数和Adam优化器,对应的实现代码如下所示。
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
(10)训练模型
对模型进行训练,遍历数据加载器中的训练数据,计算模型的预测输出和损失,并通过反向传播和优化器更新模型参数。对应的实现代码如下所示。
for epoch in range(num_epochs):
model.train()
hidden = model.init_hidden(batch_size)
for inputs, targets in data_loader:
optimizer.zero_grad()
hidden = tuple(h.detach() for h in hidden)
outputs, hidden = model(inputs, hidden)
loss = criterion(outputs.view(-1, output_size), targets.view(-1))
loss.backward()
optimizer.step()
if (epoch+1) % 10 == 0:
print(f"Epoch {epoch+1}/{num_epochs}, Loss: {loss.item()}")
(11)生成新文本
使用循环神经网络(RNN)生成新的文本,基于给定的初始文本序列,通过训练模型来预测下一个字符,并将其添加到生成的文本中,逐步生成更长的文本。对应的实现代码如下所示。
model.eval()
hidden = model.init_hidden(1)
start_seq = "From fairest creatures we desire increase,"
generated_text = start_seq
with torch.no_grad():
input_seq = torch.tensor([char_to_idx[ch] for ch in start_seq], dtype=torch.long).view(1, -1)
while len(generated_text) < 500:
output, hidden = model(input_seq, hidden)
_, predicted_idx = torch.max(output, 1)
predicted_ch = idx_to_char[predicted_idx.item()]
generated_text += predicted_ch
input_seq = torch.tensor([predicted_idx.item()], dtype=torch.long).view(1, -1)
print("Generated Text:")
print(generated_text)
执行后会输出:
Epoch 10/200, Loss: 0.19633837044239048
Epoch 20/200, Loss: 0.2718656063079838
Epoch 30/200, Loss: 0.19633837044239088
Epoch 40/200, Loss: 0.2718656063079888
Epoch 50/200, Loss: 0.19633837044239088
Epoch 60/200, Loss: 0.2718656063079834
Epoch 70/200, Loss: 0.19633837044239048
Epoch 80/200, Loss: 0.2718656063079888
Epoch 90/200, Loss: 0.19633837044239048
Epoch 100/200, Loss: 0.2718656063079838
Epoch 110/200, Loss: 0.19633837044239048
Epoch 120/200, Loss: 0.2718656063079838
Epoch 130/200, Loss: 0.19633837044239048
Epoch 140/200, Loss: 0.27186560630798348
Epoch 150/200, Loss: 0.196338370442390448
Epoch 160/200, Loss: 0.2718656063079838
Epoch 170/200, Loss: 0.19633837044239048
Epoch 180/200, Loss: 0.2718656063079888
Epoch 190/200, Loss: 0.19633837044239888
Epoch 200/200, Loss: 0.2718656063078888
Generated Text: From fairest creatures we desire increase
8.4.2 历史行为序列的特征提取
在基于循环神经网络(RNN)的推荐模型中,历史行为序列的特征提取是非常重要的一步。通过提取有用的特征,模型能够更好地理解用户的行为模式和兴趣,从而进行更准确的推荐。
下面是一些常用的提取历史行为序列特征的方法:
- Embedding(嵌入层):将用户和物品的索引转换为稠密的低维向量表示。这样可以将离散的用户和物品表示转换为连续的向量空间,使模型能够更好地理解它们之间的关系。
- 时间特征:将时间信息作为特征输入模型。例如,可以提取用户行为发生的时间戳的小时、星期几、季节等信息作为模型的输入特征。这样模型可以学习到不同时间段用户行为的变化模式。
- 历史行为统计特征:对历史行为序列进行统计特征提取,例如总交互次数、平均评分、最后一次交互时间距离当前时间的间隔等。这些统计特征能够提供关于用户行为习惯和兴趣的信息。
- 序列建模特征:使用RNN模型对历史行为序列进行建模,从中提取隐层表示作为特征。常用的RNN单元有LSTM、GRU等,它们能够捕捉序列中的时序关系和长期依赖。
- 注意力机制(Attention):在RNN模型中引入注意力机制,以便模型能够对历史行为序列中的不同部分给予不同的重要性。注意力机制可以帮助模型更关注与当前推荐任务相关的历史行为。
上述提取特征的方法既可以单独使用,也可以组合在一起形成更丰富的特征表示。根据具体的任务和数据集特点,可以选择适合的特征提取方法,并结合模型的架构进行特征工程。
在Python程序中,可以使用常见的深度学习框架(如TensorFlow和PyTorch)来实现RNN模型和特征提取。这些框架提供了丰富的工具和函数,使得特征提取和模型构建变得更加便捷和高效。例如下面是一个使用PyTorch实现的基于RNN的推荐系统例子,其中展示了RNN模型和特征提取功能的用法。
源码路径:daima/8/liti.py
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
# 读取数据
data = pd.read_csv('ratings.csv')
# 将用户和物品映射到整数索引
user_ids = data['user_id'].unique().tolist()
user2idx = {user_id: idx for idx, user_id in enumerate(user_ids)}
idx2user = {idx: user_id for idx, user_id in enumerate(user_ids)}
item_ids = data['item_id'].unique().tolist()
item2idx = {item_id: idx for idx, item_id in enumerate(item_ids)}
idx2item = {idx: item_id for idx, item_id in enumerate(item_ids)}
# 构建用户-物品序列数据
sequences = []
for _, row in data.iterrows():
user_id = row['user_id']
item_id = row['item_id']
user_idx = user2idx[user_id]
item_idx = item2idx[item_id]
sequences.append((user_idx, item_idx))
# 划分序列数据为输入和目标
input_sequences = sequences[:-1]
target_sequences = sequences[1:]
# 定义数据集类
class SequenceDataset(Dataset):
def __init__(self, sequences):
self.sequences = sequences
def __len__(self):
return len(self.sequences)
def __getitem__(self, index):
user_idx, item_idx = self.sequences[index]
return user_idx, item_idx
# 创建训练集和测试集数据加载器
train_ratio = 0.8
train_size = int(train_ratio * len(input_sequences))
train_data = SequenceDataset(input_sequences[:train_size])
train_loader = DataLoader(train_data, batch_size=32, shuffle=True)
test_data = SequenceDataset(input_sequences[train_size:])
test_loader = DataLoader(test_data, batch_size=32)
# 定义RNN模型
class RNNModel(nn.Module):
def __init__(self, num_users, num_items, hidden_size):
super(RNNModel, self).__init__()
self.embedding_user = nn.Embedding(num_users, hidden_size)
self.embedding_item = nn.Embedding(num_items, hidden_size)
self.rnn = nn.GRU(hidden_size, hidden_size)
self.fc = nn.Linear(hidden_size, num_items)
def forward(self, user, item):
user_embed = self.embedding_user(user)
item_embed = self.embedding_item(item)
output, _ = self.rnn(item_embed.unsqueeze(0))
output = output.squeeze(0)
logits = self.fc(output)
return logits
# 创建RNN模型实例
num_users = len(user_ids)
num_items = len(item_ids)
hidden_size = 64
model = RNNModel(num_users, num_items, hidden_size)
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 训练模型
num_epochs = 10
for epoch in range(num_epochs):
model.train()
for user, item in train_loader:
optimizer.zero_grad()
logits = model(user, item)
loss = criterion(logits, item)
loss.backward()
optimizer.step()
print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item()}")
# 测试模型
model.eval()
with torch.no_grad():
for user, item in test_loader:
logits = model(user, item)
_, predicted = torch.max(logits, dim=1)
for i in range(len(user)):
user_idx = user[i].item()
item_idx = predicted[i].item()
user_id = idx2user[user_idx]
item_id = idx2item[item_idx]
print(f"用户 {user_id} 下一个可能喜欢的物品是 {item_id}")
在上述代码中,首先将用户和物品映射到整数索引,并构建“用户-物品”的序列数据。然后定义了一个自定义的数据集类和数据加载器来处理序列数据。接下来,创建了一个基于GRU的RNN模型,并使用交叉熵损失函数和Adam优化器进行训练。最后,使用训练好的模型对测试集进行推荐,并打印输出每个用户可能喜欢的物品。执行后会输出:
Epoch [1/10], Loss: 1.6321121454238892
Epoch [2/10], Loss: 1.5618427991867065
Epoch [3/10], Loss: 1.4931098222732544
Epoch [4/10], Loss: 1.4258830547332764
Epoch [5/10], Loss: 1.3601555824279785
Epoch [6/10], Loss: 1.295941710472107
Epoch [7/10], Loss: 1.23326575756073
Epoch [8/10], Loss: 1.1721522808074951
Epoch [9/10], Loss: 1.1126271486282349
Epoch [10/10], Loss: 1.0547196865081787
用户 1 下一个可能喜欢的物品是 202
用户 1 下一个可能喜欢的物品是 101
用户 2 下一个可能喜欢的物品是 202
用户 2 下一个可能喜欢的物品是 101
在上面的输出结果中,对于用户1,模型预测下一个可能喜欢的物品是202和101;对于用户2,模型预测下一个可能喜欢的物品是202和101。
注意:这只是一个简化的例子,在实际应用中可能需要根据数据和任务的不同进行模型调整和超参数调优。同时,我们也可以根据需要添加其他的特征提取方法来丰富特征表示。