推荐系统之FM因子分解机(记录复现和实验中的收获)

本文介绍了复现推荐系统中FM因子分解机模型的过程,涵盖了概念如测试集和验证集的作用,BCELoss和BCEWithLogitsLoss的区别。文章详细讨论了实用的PyTorch操作,如数据切片、模型参数查看、数据集划分,并深入解析了FM模型的两种实现方式。此外,文章还提到了处理数据细节时的注意事项,如数据类型、形状和维度管理,以及排查和解决代码中的错误。最后,文章提出了对模型进行实验的思路,包括比较不同模型的性能和超参数实验的顺序。
摘要由CSDN通过智能技术生成


前言

本篇文章是我在复现推荐系统中的FM因子分解机模型时的收获总结。


一、概念

1. 测试集和验证集
测试集和验证集都是用来评估模型性能的数据集,但在用途和使用方式上不同。
验证集通常用于训练过程中对模型的调优,通过在训练过程中对验证集进行评估,可以根据验证集的结果对模型进行选择和调整,比如修改超参数等。在训练集上训练后,使用验证集对模型进行评估,可以帮助我们避免过拟合。如果在训练过程中,模型的表现在验证集上开始变差,那么我们可以及时停止训练,防止模型在训练集上过拟合。
测试集则用于最终评估模型的性能,它是用来衡量模型在未知数据上的表现。在模型选择和调优完成后,使用测试集对模型进行评估,可以得到一个更准确的模型性能评估。测试集的数据应该是独立于训练集和验证集的,这样可以保证测试集的数据和模型没有任何关系,从而确保评估的结果是准确可靠的。


2. BCELoss和BCEWithLogitsLoss的区别

BCELoss要求输入值取值在 [0,1],而BCEWithLogitsLoss就是把Sigmoid和BCELoss合成为一步,且比简单地将 Sigmoid 层加上 BCELoss 损失更稳定

二、实用的操作


1. 切片

对于二维数组 arr = np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]),我们可以使用以下代码进行切片操作:

arr[1:3, 0:2]  #选取第2行和第3行,第1列和第2列,得到一个2x2的数组
arr[:, 1:] #选取所有行,第二列到最后一列
arr[:, 0] #选取所有行,第一列


2. 查看模型的参数

for param in model.parameters():
	print(param)


3. 查看变量的设备

V.device

三、函数用法及其参数


1. train_test_split:训练集和验证集划分函数

X_train, X_val, y_train, y_val = train_test_split(train_data[:, :-1], train_data[:, -1], test_size=0.2, random_state=2022)

第一个参数表示特征矩阵列,第二个参数表示标签列,第三个参数test_size表示验证集的比例
第四个参数表示随机种子值,如2022是一个随机数生成器的种子值。设置一个固定的种子值可以确保每次运行代码时,随机数生成器生成的随机数序列都是一样的。可以确保实验的可重复性。如果不使用固定的种子值,每次运行代码时生成的随机数序列都会不同,这可能会对实验的结果造成影响。


2. nn.Embedding的用法

self.embedding = nn.Embedding(input_dim, latent_dim)

a. 原理:input_dim为特征序列中元素的最大值,latent_dim为要生成的隐向量维度
self.embedding(X)先为每个特征生成隐向量(有input_dim个),产生一个大小为 input_dim * latent_dim的矩阵,即每个隐向量的维度为latent_dim,然后在这个矩阵中查找X中每个元素对应的隐向量。最后得到的结果是1 * X_dim * latent_dim(这里的1是特殊情况,因为只输入了一个样本,一般情况下是 batch_size * X_dim * latent_dim)
b. 注意事项:
self.embedding(X) 要求输入的张量必须是整数类型(Long 或 Int),倘若输入的张量X是 torch.cuda.FloatTensor 类型。可以通过将 X 转换为整数类型来解决,即 X.long(),例如 self.embedding(X.long())。


3. torch.sum的用法

torch.sum(X, dim = 0)

在矩阵X中按行或者按列求和,若dim=0则按行求和,dim=1则按列求和
torch.sum就是在矩阵的中进行元素求和的按行或按列的批量操作,更精准地说,此函数能够实现并行计算

在FM中的应用:

torch.sum(torch.matmul(x, self.embedding(x)), dim = 1)


4. torch.matmul(A,A)与A.pow(2)的区别
torch.matmul(A, B) 表示矩阵乘法,对于输入的两个张量A和B(它们需要满足矩阵乘法的形状条件)、对于A和B形状分别为(m, p)和(p, n)的两个矩阵,它们的乘积结果为一个形状为(m, n)的矩阵。
torch.matmul(A, A)实现的是A与A的矩阵乘法
而A.pow(2)则是对张量A中的每一个元素进行平方操作。对于形状为(m, n)的张量A,执行A.pow(2)操作后,返回的张量形状仍然为(m, n),且每个元素都是原来的平方。
在FM模型中,需要对embedding层的输出进行平方操作,这里使用A.pow(2)可以实现平方的操作。而如果使用torch.matmul(A, A)的话,会导致维度错误,因为矩阵乘法要求输入的两个矩阵的形状要满足一定的条件。


5. get_chunk:读取部分数据集

data_df = pd.read_csv(file, sep='\t', iterator=True, header=None, names=names)
train_df = train_df.get_chunk(1600)

a. names参数要自定义
b. 用df.get_chunk(1600)来自定义选取多少行,上述代码是选取了1600行


6. insert:在Dataframe中插入新的一列

df.insert(loc=0, column='Label', value=-1)
#表示在第一列插入列名为Label、值为-1的列


7. drop:去掉id列

data.drop(['id'], axis=1, inplace=True)

在一般的数据集中,可以把’id’列去掉,避免影响后续对df的操作
但在原始的criteo数据集中不需要,因为criteo中没有id列


8. 自定义Dataset列

a. init 函数要有返回值 return;
b. 必须要实现 lengetitem 函数
c. 没有 drop 函数
d. getitem(self, idx) 是 Dataset 类中的一个方法,用于定义如何通过索引获取数据集中的样本。
具体来说,getitem(self, idx) 方法应该接受一个索引值 idx,返回该索引对应的一个样本。这个样本可以是一个元组或字典,其中包含了输入特征和对应的标签。当使用 PyTorch 中的 DataLoader 加载数据集时,DataLoader 会自动调用 Dataset 的 getitem 方法来获取每一个样本。在训练时,DataLoader 会自动地对数据进行 shuffle 和 batch,以提高模型的训练效率。
d. 突然发现根本没有必要自己定义一部分读取dataset类,巨麻烦,有时间再弄一个,方便日后用
这是已完成的部分:

#读入部分数据
class DataReadPart(Dataset):
    def __init__(self, file, read_part=True, sample_num=10000):
        self.read_part = read_part
        self.file = file
        self.sample_num = sample_num
        
        if read_part:
            data_df = pd.read_csv(file, sep='\t', iterator=True, header=None, names=names)
            data_df = data_df.get_chunk(sample_num)
        else:
            data_df = pd.read_csv(file, sep='\t', header=None, names=names)
        
        #在读入数据时就把id列去掉
        self.data_df = data_df.drop(['id'], axis=1)
    
    #读取数据
train_df = DataReadPart(data_path + 'train.txt', read_part=True, sample_num=1600)
test_df = DataReadPart(data_path + 'test.txt', read_part=True, sample_num=400)

#对整体数据集处理
df.test['Label'] = -1
data = pd.concat([df_train, df_test], axis = 0)
data.fillna(-1, inplace = true)

#对特征的处理
cols = data.columns.values
dense_feats = [f for f in cols if f[0] == 'I']
sparse_feats = [f for f in cols if f[0] == 'C']


9. type():查看数据的类型


10. df.values.astype(‘float32’)
将DataFrame数据类型转变为float32的numpy数组类型


11. np.shape(arr):查询行列形状


12. nn.Linear的用法
a. 注意事项:self.linear(X)是一个线性层,它要求输入的张量的数据类型为浮点型。默认情况下,PyTorch 的张量类型是 torch.FloatTensor。如果输入的张量数据类型不是 torch.FloatTensor,需要先将其转换为 torch.FloatTensor,例如通过 inputs.float()。


13. for step, (X, y) in enumerate(train_loader, 1)
用于模型训练,step表示训练过程中次数的索引
X表示特征矩阵,y表示标签
不能写成 for (X, y) in enumerate(train_loader, 1):

四、FM模型的两种不同写法

#修改版1:采用自定义权重矩阵
class FM(nn.Module):
    def __init__(self, input_dim, latent_dim):
        super(FM, self).__init__()
        self.linear = nn.Linear(input_dim, 1, bias = True)
        self.v = nn.Parameter(torch.randn(input_dim, latent_dim))
        self.sigmoid = nn.Sigmoid()
    def forward(self, X):
        linear_term = self.linear(X).squeeze(1)
        interact_fir = torch.sum(torch.mm(X,self.v).pow(2),dim = 1)
        interact_sec = torch.sum(torch.mm(X.pow(2),self.v.pow(2)), dim = 1)
        out = linear_term + 0.5 * (interact_fir - interact_sec)
        output = self.sigmoid(out)
        return output
#修改版2:采用embedding隐向量矩阵
class FM(nn.Module):
    def __init__(self, input_dim, latent_dim):
        super(FM, self).__init__()
        self.linear = nn.Linear(input_dim, 1)
        self.embedding = nn.Embedding(39, latent_dim)
        self.sigmoid = nn.Sigmoid()    
    def forward(self, X):
        linear_term = self.linear(X).squeeze(1)
        emb = self.embedding(feat_id)
        interact_fir = torch.sum(torch.mm(X,emb).pow(2),dim = 1)
        interact_sec = torch.sum(torch.mm(X.pow(2),emb.pow(2)), dim = 1)
        out = linear_term + 0.5 * (interact_fir - interact_sec)
        output = self.sigmoid(out)
        return output

五、处理细节


1. 处理数据集的时候,要注意前后格式的一致性

应用:训练集和测试集在没有表头的时候把训练集的表头装到了测试集上(测试集原本没有label列)

names = ['Label'] + ['I' + str(i) for i in range(1, 14)] + ['C' + str(i) for i in range(1, 27)]
test_df = pd.read_csv(data_path + 'train.txt', sep='\t', iterator=True, header=None, names=names)


2. 代码中的书写错误

a. 在 process_feat() 函数中,对于稀疏特征的处理,变量名写错了。应该是 sparse_feats 而不是 sparse_dense。
b. 在 process_feat() 函数中,对于稀疏特征的处理,LabelEncoder 的拼写有误。应该是 LabelEncoder() 而不是 LableEncoder()。
c. train_test_split中的split不要写成spilt


3. 不要重复运行同一个单元格,可能引起报错

def process_feat(data, dense_feats, sparse_feats):
    df_dense = df[dense_feats].fillna(0.0)
    #......中间内容省略
    return df_new.values.astype('float32')
data = process_feat(data, dense_feats, sparse_feats)

运行一次 data的类型从df变成了numpy,第二次重复运行,输入的df为numpy数组,对df[dense_feats]索引出错,因为dense_feats是列名,numpy数组只能对数值进行索引
因此建议在重复运行代码前先清空变量或重启Kernel。


4. 函数名称:开头大写

一些特殊的函数首字母要开头大写,不然会报错
nn.Linear
nn.Embedding
nn.Sigmoid


5. 矩阵的形状和维度:在不同层的连接时可能会产生影响,处理不当的话会报错(而且在报错的时候不容易找出原因)

[64]表示一个包含64个元素的一维张量,可以看作是一个长度为64的向量。而[64,1]表示一个包含64个元素的二维张量,其中第一维表示行数,第二维表示列数,本例中第一维为64,第二维为1,可以看作是一个长度为64的列向量。两者形状不同,因此在某些情况下需要进行维度变换或者广播操作才能进行计算。

实际场景举例:

self.linear = nn.Linear(input_dim, 1, bias = True)
linear_term = self.linear(X)

经过此linear层,将得到一个[batch_size, 1]的矩阵,即一个二维张量

self.linear = nn.Linear(input_dim, 1, bias = True)
linear_term = self.linear(X).squeeze(1)

经过此linear层,将得到一个[batch_size]的张量,是一个一维张量

linear层的输出没处理好的话会在执行pred(model(X), y)时报错,其中y是[batch_size]的一维张量

六、bug的排查分析与解决


1. TypeError

df_dense[f] = df_dense[f].apply(lambda x: np.log(1 + x) if x > -1  else 0)
TypeError: '>' not supported between instances of 'str' and 'int'


报错:这个报错通常表示出现了不支持的数据类型。在这里,根据代码中的 > 符号,可以推测出问题可能是在进行数值比较时出现了字符串类型的变量,因为在 Python 中字符串类型是不支持与数值类型进行比较的。
分析:根据代码,报错出现在对 dense 特征进行处理时,使用了 apply() 函数进行数值处理,因此,在 apply() 函数中输入的x可能存在字符串类型的值,与数值类型的值进行比较时报错。
那么为什么会出现字符串类型?nan是没事的,字符串一定是C1-C26中出现的,但在df_dense中元素都是数值型的。这说明有原本应属于df_sparse中的数据被加入到了df_dense中,那么可以推断是df_dense的提取出错了,可以把错误范围缩小到:

names = ['Label'] + ['I' + str(i) for i in range(1, 14)] + ['C' + str(i) for i in range(1, 27)]
#读取部分数据
train_df = pd.read_csv(data_path + 'train.txt', sep='\t', iterator=True, header=None, names=names)
train_df = train_df.get_chunk(1600)

test_df = pd.read_csv(data_path + 'test.txt', sep='\t', iterator=True, header=None, names= names)
test_df = test_df.get_chunk(400)
#test_df.insert(loc=0, column='Label', value=-1)

data = pd.concat([train_df, test_df], axis = 0)
print(test_df)

#对特征的处理
cols = data.columns.values
dense_feats = [f for f in cols if f[0] == 'I']
sparse_feats = [f for f in cols if f[0] == 'C']

可以发现train_df没问题,而test_df使用了包含’Label’的表头,而test_df是没有标签列的
解决:为test_df加上标签列,值定义为-1表示缺失


2. IndexError:索引的类型不符合

def process_feat(data, dense_feats, sparse_feats):
    df_dense = df[dense_feats].fillna(0.0)
    #......中间内容省略
    return df_new.values.astype('float32')
data = process_feat(data, dense_feats, sparse_feats)
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In [5], line 1
----> 1 data = process_feat(data, dense_feats, sparse_feats)
      3 #数据集划分
      4 
      5 #将Data划分为训练集和测试集
      6 train_data = data[data[:,0] != -1]

Cell In [3], line 5, in process_feat(data, dense_feats, sparse_feats)
      2 df = data.copy()
      4 #对dense特征的处理
----> 5 df_dense = df[dense_feats].fillna(0.0)
      6 for f in dense_feats:
      7     df_dense[f] = df_dense[f].apply(lambda x: np.log(1 + x) if x > -1  else -1)

IndexError: only integers, slices (`:`), ellipsis (`...`), numpy.newaxis (`None`) and integer or boolean arrays are valid indices

运行一次 data的类型从df变成了numpy,第二次重复运行,输入的df为numpy数组,对df[dense_feats]索引出错,因为dense_feats是列名,numpy数组只能对数值进行索引
因此建议在重复运行代码前先清空变量或重启Kernel。


3. RuntimeError

data = pd.concat([train_df, test_df], axis = 0)
def process_feat(data, dense_feats, sparse_feats):
    ###中间内容省略###
    #df_new = pd.concat([df['Label'], df_dense, df_sparse], axis = 1)
    df_new = pd.concat([df_dense, df_sparse], axis = 1)
    return df_new.values.astype('float32')
data = process_feat(data, dense_feats, sparse_feats)

class FM(nn.Module):
    def __init__(self, input_dim, latent_dim):
        super(FM, self).__init__()
        self.linear = nn.Linear(input_dim, 1)
        #后面省略
    def forward(self, X):
        linear_term = self.linear(X)
        #后面省略
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
Cell In [10], line 18
     16 X, y = X.cuda(), y.cuda()
     17 optimizer.zero_grad()
---> 18 pred = model(X)
     19 pred = pred.squeeze(-1)
     20 loss = loss_func(pred, y)

File C:\newD\D\AAna\envs\pytorch\lib\site-packages\torch\nn\modules\module.py:1130, in Module._call_impl(self, *input, **kwargs)
   1126 # If we don't have any hooks, we want to skip the rest of the logic in
   1127 # this function, and just call forward.
   1128 if not (self._backward_hooks or self._forward_hooks or self._forward_pre_hooks or _global_backward_hooks
   1129         or _global_forward_hooks or _global_forward_pre_hooks):
-> 1130     return forward_call(*input, **kwargs)
   1131 # Do not call functions when jit is used
   1132 full_backward_hooks, non_full_backward_hooks = [], []

Cell In [9], line 9, in FM.forward(self, X)
      8 def forward(self, X):
----> 9     linear_term = self.linear(X)
     10     interact_fir = torch.sum(torch.matmul(X,self.embedding).pow(2),dim = 1)
     11     interact_sec = torch.sum(torch.matmul(X.pow(2),self.embedding.pow(2)), dim = 1)

File C:\newD\D\AAna\envs\pytorch\lib\site-packages\torch\nn\modules\module.py:1130, in Module._call_impl(self, *input, **kwargs)
   1126 # If we don't have any hooks, we want to skip the rest of the logic in
   1127 # this function, and just call forward.
   1128 if not (self._backward_hooks or self._forward_hooks or self._forward_pre_hooks or _global_backward_hooks
   1129         or _global_forward_hooks or _global_forward_pre_hooks):
-> 1130     return forward_call(*input, **kwargs)
   1131 # Do not call functions when jit is used
   1132 full_backward_hooks, non_full_backward_hooks = [], []

File C:\newD\D\AAna\envs\pytorch\lib\site-packages\torch\nn\modules\linear.py:114, in Linear.forward(self, input)
    113 def forward(self, input: Tensor) -> Tensor:
--> 114     return F.linear(input, self.weight, self.bias)

RuntimeError: mat1 and mat2 shapes cannot be multiplied (32x38 and 39x1)

报错:矩阵乘法无法进行(因为两个矩阵的大小有误,无法进行操作)
分析:运行 self.linear(X) 这行代码时,输入的 X 的维度是 (batch_size, 32, 38),而 self.linear 的权重矩阵的维度是 (input_dim, 1),在执行矩阵乘法时,第一个矩阵的列数应该等于第二个矩阵的行数,但是在这里第一个矩阵的列数是 38,第二个矩阵的行数是 input_dim = 39,因此无法完成矩阵乘法操作,从而抛出了 mat1 and mat2 shapes cannot be multiplied 的错误。由此错误定为到了输入的X矩阵格式错误
解决:可能是由于数据预处理时出现了错误,导致输入的维度不符合模型的输入要求。因此我们去检查数据的维度和预处理过程,并确保输入的维度与模型的输入要求相匹配。检查process_deat时发现,输入的data是有Label列的,但输出的df_new却只包含了特征少了Label列,这样的操作使得后面再进行数据集的划分、处理时少一列特征,导致X的维度变成 (batch_size, 32, 38)。所以解决办法就是把Label列加上


4. 模型训练时loss偏大并且保持不变
解决过程:
检查:parameter根本没发生变化,也就是说参数根本没得到训练
此时超参数设置:batch_size = 64, K = 30, lr = 0.001, len(train_dataset) = 128000,采用loss_func计算损失

第一次分析:可能是lr不合适,尝试调高lr
第一次结果:调高lr到0.01,参数还是没得到训练,loss始终不变且偏大

第二次分析:可能是数据量太小了,尝试调大数据集
第二次结果:参数还是没得到训练,loss始终不变且偏大

第三次分析:可能是loss_func的问题,换成criterion
第三次结果:参数还是没得到训练,loss始终不变,但回归到正常范围
(loss偏大的问题解决了,说明对loss_func和criterion的用法还是没理解)

第四次分析:可能是batch_size太大了,数据集又比较小,调小batch_size至8
第四次结果:参数还是没得到训练

第五次分析:数据没有归一化,导入MinMaxScaler进行归一化
第五次结果:参数得到了训练,问题暂时解决


5. (上面4的后续问题)进行实验时loss偏大且一下子就收敛了,一直稳定在0.693下不去,但有梯度,parameter也在更新
分析1:学习率太小了,调大学习率
结果1:没用

分析2:batch_size太大了
结果2:没用

分析3:K太大了
结果3:没用

分析4:loss函数有问题,把criterion改成了loss_func
结果4:loss终于开始下降了

总结:我原本的模型中最后用了sigmoid,然后在模型训练时又用了BCEWithLogitsLoss,相当于用了两次sigmoid,所以产生错误

七、对模型进行实验

1. 不同模型性能的比较

不同模型的实用场景不同,那么数据集处理的方式也是不同的。比如要解决分类任务:对于FM模型来说,评分数据集应该处理成特征矩阵;对于矩阵分解来说,评分数据应该处理成user-item的评分矩阵(共现矩阵)。那么如果要进行FM和协同过滤这两种模型性能的比较,我的想法是:对于Movielens-1m这一原始数据集,分别处理成特征矩阵和user-item评分矩阵这两种格式,在两个模型中分别进行训练,最后用均方根误差(Root Mean Square Error,RMSE)或平均绝对误差(Mean Absolute Error,MAE)来衡量模型的好坏。总的来说,我的疑问解决了,原本的疑惑是:MF的loss和MF的loss可以进行比较吗(两个模型产出的loss是衡量不同模型的通用标准吗),现在可以说,衡量一个模型的好坏,就可以用RMSE或者MAE来实现,是通用的,不依赖于模型本身,也不依赖于数据处理的方式。

2. 在实验过程中,对不同超参数实验的先后顺序应该是怎么样的(还未解决)

比如在隐向量因子数量K:8,batch_size:32,验证集比例:0.2,epochs:20的条件下,对学习率lr进行实验
假设得出了结论lr= 0.003时,模型效果最好

接着对隐向量因子数量K进行实验,同样以batch_size:32,验证集比例:0.2,epochs:20为条件,学习率lr能就固定为0.003吗,还是说对不同的K,lr还要继续进行训练。

换句话说,在K=8时,不同的学习率中,lr = 0.003效果
那么在K=16时,lr=0.003的效果是不是也是最好的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值