搜集到click-buy数据集,
数据集分享在网盘
通过百度网盘分享的文件:数据集_20241031_220915
链接:https://pan.baidu.com/s/1qcXAO_P1h3Vrrui5qFbYLw?pwd=6f3m
其中 yoochoose-buys.dat
特征含义buy_df.columns = ['session_id', 'timestamp', 'item_id', 'price', 'quantity']
420374,2014-04-06T18:44:58.314Z,214537888,12462,1
yoochoose-clicks.dat
特征含义click_df.columns = ['session_id', 'timestamp', 'item_id', 'category']
1,2014-04-07T10:51:09.277Z,214536502,0
我们需要构建图结构通过训练该图神经网络来预测click-buy的关系
构建训练用的Dataset, 在下面代码模板上重构process 方法,用于处理原始数据并将其转化为图数据。
from torch_geometric.data import InMemoryDataset
import torch
class ChooseBinaryDataset(InMemoryDataset):
def __init__(self, root, clicks_file, buys_file, transform=None, pre_transform=None):
self.clicks_file = clicks_file
self.buys_file = buys_file
super(ChooseBinaryDataset, self).__init__(root, transform, pre_transform)
self.data, self.slices = self.process()
@property
def processed_file_names(self):
return ['data.pt'] # 指定处理后保存的数据文件名
def download(self):
pass
def process(self):
# Implement your data processing logic here
pass
代码的按照流程完成下面任务
-
数据分组:
使用groupby
方法按session_id
将原始数据分成多个会话组,便于处理每个用户的行为序列。 -
标签编码:
使用LabelEncoder
对商品 ID 进行编码,将原始字符串转换为整数形式,便于后续处理。 -
节点特征:
从每个会话组中提取商品 ID(item_id
)并创建节点特征,确保每个商品 ID 唯一。 -
边缘索引:
创建源节点和目标节点,用于构建图中的边缘。源节点是会话中的前一个商品,目标节点是后续商品。 -
图数据对象:
使用 PyTorch Geometric 的Data
类构建图数据对象,并将其添加到data_list
中。 -
数据保存:
使用collate
方法将图数据对象合并,并保存到指定的路径。
def process(self):
# 加载数据
clicks_df = pd.read_csv(self.clicks_file, sep=';', names=['session_id', 'timestamp', 'item_id', 'category'])
buys_df = pd.read_csv(self.buys_file, sep=';', names=['session_id', 'timestamp', 'item_id', 'price', 'quantity'])
# 创建购买标签
buys_df['buy'] = 1 # 标记购买的商品
clicks_df = clicks_df.merge(buys_df[['session_id', 'item_id', 'buy']], on=['session_id', 'item_id'], how='left')
clicks_df['buy'] = clicks_df['buy'].fillna(0) # 填充未购买的商品为0
# 存储图数据对象
data_list = []
grouped = clicks_df.groupby('session_id')
for session_id, group in tqdm(grouped, desc="Processing sessions"):
# 标签编码
sess_item_id = LabelEncoder().fit_transform(group.item_id)
group = group.reset_index(drop=True)
group['sess_item_id'] = sess_item_id
# 节点特征
node_features = group['sess_item_id'].values
node_features = torch.LongTensor(node_features).unsqueeze(1)
# 创建边缘索引
target_nodes = group.sess_item_id.values[1:]
source_nodes = group.sess_item_id.values[:-1]
edge_index = torch.tensor([source_nodes, target_nodes], dtype=torch.long)
# 创建图数据对象
x = node_features
y = torch.FloatTensor([group.buy.values[0]]) # 使用第一个商品的购买标签
data = Data(x=x, edge_index=edge_index, y=y)
data_list.append(data)
data, slices = self.collate(data_list)
torch.save((data, slices), self.processed_paths[0])
return data, slices
clicks_df
包含会话 ID (session_id
)、时间戳 (timestamp
)、商品 ID (item_id
) 和类别 (category
)。buys_df
包含会话 ID、时间戳、商品 ID、价格 (price
) 和数量 (quantity
)。
- 在购买数据框中为每个购买的商品添加标签
buy
,值为 1。对点击数据框进行合并,将购买标签添加到点击数据中。对未购买的商品,buy
标签填充为 0。 - 按
session_id
对点击数据进行分组,以便逐会话处理。遍历每个session的点击数据。将商品 ID 转换为标签编码,生成sess_item_id
,便于后续处理。
group = group.reset_index(drop=True)
- 重置索引并将编码后的商品 ID 添加到数据框中。
target_nodes = group.sess_item_id.values[1:]
source_nodes = group.sess_item_id.values[:-1]
edge_index = torch.tensor([source_nodes, target_nodes], dtype=torch.long)
上面是对click 1 -> click 2 -> click 3 -> ...-> click N 这样的链表数据做处理,构建出source-> target的图结构的边关系
node_features = group['sess_item_id'].values
node_features = torch.LongTensor(node_features).unsqueeze(1)
session 里的所有点击item 作为该session的节点特征
下面就是样本和标签的定义
x = node_features
y = torch.FloatTensor([group.buy.values[0]])
节点特征作为样本,session里购买的商品取排序第一个商品作为标签然后转为torch张量
data = Data(x=x, edge_index=edge_index, y=y)
data_list.append(data)
图结构这样构建出来以后,把它都加入到data_list
data, slices = self.collate(data_list)
torch.save((data, slices), self.processed_paths[0])
- 将所有图数据对象合并为一个批处理(使用
collate
函数)。
torch_geometric
中,collate
方法用于将多个图数据对象(Data
)合并成一个批量(batch)
collate
方法会将每个图的数据(例如节点特征 x
)合并成一个大的张量。它会在节点特征的维度上进行拼接,以便在一个批次中包含来自多个图的信息。边缘索引(edge_index
)的合并也会进行调整。对于每个图,边缘索引会被重定向,以保证在新的批次中每个图的节点索引是唯一的,除了节点特征和边缘索引,collate
方法还会处理图数据对象中的其他属性,比如图的标签 y
、图的边缘特征 edge_attr
等,确保它们在合并后仍能对应到正确的图。
- 保存处理后的数据和切片信息,以便后续使用。
构建图神经网络模型,该模型参考额SAGE项目里的图模型, 需要有下面层结构
- 卷积层 (
SAGEConv
):定义了三个图卷积层,用于提取节点特征。 - 池化层 (
TopKPooling
):用于降低图的复杂度,减少节点数量。 - 嵌入层 (
Embedding
):用于将商品 ID 映射到低维空间。 - 线性层 (
Linear
):用于特征转换和输出。 - 标准化层 (
BatchNorm1d
) 和激活函数 (ReLU
):用于加速训练和增加模型的非线性能力。
class GraphNet(torch.nn.Module):
"""Graph Neural Network for binary classification tasks."""
def __init__(self, emb_dim):
super(GraphNet, self).__init__()
# 定义图卷积层
self.conv1 = SAGEConv(emb_dim, 128)
self.pool1 = TopKPooling(128, ratio=0.8)
self.conv2 = SAGEConv(128, 128)
self.pool2 = TopKPooling(128, ratio=0.8)
self.conv3 = SAGEConv(128, 128)
self.pool3 = TopKPooling(128, ratio=0.8)
# 定义嵌入层
self.item_embedding = torch.nn.Embedding(num_embeddings=emb_dim + 10, embedding_dim=emb_dim)
# 定义线性层
self.lin1 = torch.nn.Linear(128, 128)
self.lin2 = torch.nn.Linear(128, 64)
self.lin3 = torch.nn.Linear(64, 1)
# 定义标准化层和激活函数
self.bn1 = torch.nn.BatchNorm1d(128)
self.bn2 = torch.nn.BatchNorm1d(64)
self.act1 = torch.nn.ReLU()
self.act2 = torch.nn.ReLU()
def forward(self, data):
x, edge_index, batch = data.x, data.edge_index, data.batch
# 使用嵌入层将节点特征进行编码
x = self.item_embedding(x).squeeze(1) # n * 128
# 第一个卷积层和池化
x = F.relu(self.conv1(x, edge_index))
x, edge_index, _, batch, _, _ = self.pool1(x, edge_index, None, batch)
x1 = gap(x, batch) # 全局池化
# 第二个卷积层和池化
x = F.relu(self.conv2(x, edge_index))
x, edge_index, _, batch, _, _ = self.pool2(x, edge_index, None, batch)
x2 = gap(x, batch) # 全局池化
# 第三个卷积层和池化
x = F.relu(self.conv3(x, edge_index))
x, edge_index, _, batch, _, _ = self.pool3(x, edge_index, None, batch)
x3 = gap(x, batch) # 全局池化
# 将三个尺度的全局特征相加
x = x1 + x2 + x3
# 通过线性层进行特征转换
x = self.act1(self.lin1(x))
x = self.act2(self.lin2(x))
x = F.dropout(x, p=0.5, training=self.training)
# 最后一层使用Sigmoid激活函数进行二元分类
x = torch.sigmoid(self.lin3(x)).squeeze(1) # batch个结果
return x
- 特征提取:首先通过嵌入层将节点特征编码,然后经过多个图卷积层和池化层提取特征。
- 池化操作:通过
gap
函数获取全局特征。 - 特征融合:将不同尺度的全局特征相加,以获得综合特征。
- 线性变换和激活:通过多个线性层和激活函数进行最终的特征处理。
- 输出:最后通过 Sigmoid 激活函数将输出压缩到 [0, 1] 之间,适合二分类任务。
编写训练代码
model = GraphNet(emb_dim=100).to(device) # 使用适当的嵌入维度
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
criterion = torch.nn.BCELoss()
# 创建数据加载器
train_loader = DataLoader(dataset, batch_size=32, shuffle=True)
# 训练循环
for epoch in range(100): # 设置适当的 epoch 数
model.train()
total_loss = 0
for data in train_loader:
data = data.to(device)
optimizer.zero_grad()
output = model(data)
loss = criterion(output, data.y)
loss.backward()
optimizer.step()
total_loss += loss.item()
print(f'Epoch {epoch + 1}, Loss: {total_loss / len(train_loader)}')
torch.save(model.state_dict(), 'graphnet_model.pth')
print("Model saved to graphnet_model.pth")
开始训练
图结构比较大,训练时间较长
看到图神经网络的loss可以训练的特别小
用相同构造图数据的方法构造下测试数据,加载训练的模型尝试进行预测
# 加载模型
import numpy as np
import pandas as pd
import torch
from sklearn.preprocessing import LabelEncoder
from torch_geometric.data import Data
from torch_geometric.loader import DataLoader
from tqdm import tqdm
from model import GraphNet
if torch.cuda.is_available():
device = "cuda"
else:
device = "cpu"
loaded_model = GraphNet(emb_dim=100).to(device)
loaded_model.load_state_dict(torch.load('graphnet_model.pth'))
loaded_model.eval() # 设置为评估模式
# 准备预测数据(假设你有新的点击数据)
# 这里你可以使用与训练时相同的处理方式来创建新的 Data 对象
# 例如,假设你有新的点击数据文件
new_clicks_df = pd.read_csv('test-yoochoose-clicks.dat', sep=';', names=['session_id', 'timestamp', 'item_id', 'category'])
# 创建新的图数据
new_data_list = []
for session_id, group in tqdm(new_clicks_df.groupby('session_id'), desc="Processing new sessions"):
sess_item_id = LabelEncoder().fit_transform(group.item_id)
group = group.reset_index(drop=True)
group['sess_item_id'] = sess_item_id
# 节点特征
node_features = group['sess_item_id'].values
node_features = torch.LongTensor(node_features).unsqueeze(1)
# 创建边缘索引
if len(group) > 1: # 确保有足够的节点以构建边
target_nodes = group.sess_item_id.values[1:]
source_nodes = group.sess_item_id.values[:-1]
edge_index = torch.tensor([source_nodes, target_nodes], dtype=torch.long)
# 创建图数据对象
x = node_features
new_data = Data(x=x, edge_index=edge_index)
new_data_list.append(new_data)
# 将新的图数据转换为 DataLoader
new_loader = DataLoader(new_data_list, batch_size=32, shuffle=False)
# 开始预测
predictions = []
with torch.no_grad(): # 禁用梯度计算以节省内存
for data in new_loader:
data = data.to(device)
output = loaded_model(data)
predictions.append(output.cpu().numpy())
# 将预测结果合并
predictions = np.concatenate(predictions)
# 打印预测结果
print("Predictions:", predictions)
代码提交在我的github上
https://github.com/chenrui2200/click_buy_graph_predict