机器学习算法实现解析——libFM之libFM的训练过程之Adaptive Regularization
本节主要介绍的是libFM源码分析的第五部分之二——libFM的训练过程之Adaptive Regularization的方法。
5.3、Adaptive Regularization的训练方法
5.3.1、SGD的优劣
在“机器学习算法实现解析——libFM之libFM的训练过程之SGD的方法”中已经介绍了基于SGD的FM模型的训练方法,SGD的方法的最大优点是其训练过程很简单,只需在计算的过程中求解损失函数对每一个参数的偏导数,从而实现对模型参数的修改。
我们都知道,FM模型对正则化参数的选择比较敏感,在SGD的训练方法中,正则化参数是通过事先指定的,选择的优劣直接影响到训练的最终效果,那么,是否存在一种方法,能够自动选择正则化参数呢?此时,可以使用Adaptive Regularization的参数训练方法。
5.3.2、Adaptive Regularization方法的理论
对于SGD的方法,在学习的过程中,将损失函数l分别对常数项的参数w0,一次项的参数wi以及交叉项的参数wi求偏导,并利用梯度下降法更新模型中的对应参数,值得注意的是,这里的正则化参数是事先指定的,如常数项的正则化参数为λ0,一次项的正则化参数为λw以及交叉项的正则化参数为λf。基于SGD的训练过程如下所示:
Adaptive Regularization方法提出将正则化参数的选择过程融入到模型参数的修改过程中,达到同时修正模型的参数和正则化参数的过程。为了能够做到这点,首先需要将训练数据集S区分为用于训练模型的训练集ST和验证集SV,且S=ST∪SV。
已知FM模型可以表示为:
其中,Θt+1表示的是第t+1代时所有模型参数的集合,包括:wt+10,wt+1i以及vt+1f。然而每一个参数都是由第t代时的参数经过SGD更新生成的,即:
将上述的更新公式代入FM模型的计算公式中,得到:
当固定正则化参数λ0,λi以及λf时,与“机器学习算法实现解析——libFM之libFM的训练过程之SGD的方法”中所述的SGD方法一致,此时,当更新完一遍模型的参数,此时,固定模型中的参数,并利用验证集SV和SGD方法更新正则化参数:
对于回归问题:
对于分类问题:
而∂y^(i)∂λ对于不同的λ,其值为:
那么,对于Adaptive Regularization的具体过程如下所示:
5.3.3、Adaptive Regularization方法的实现
Adaptive Regularization方法也是一种基于梯度的方法,因此其实现类fm_learn_sgd_element_adapt_reg
类也继承自fm_learn_sgd
类,其类之间的关系如下图所示:
Adaptive Regularization方法的实现在文件fm_learn_sgd_element_adapt_reg.h
中,文件fm_learn_sgd_element_adapt_reg.h
中实现了fm_learn_sgd_element_adapt_reg
类,该类继承自fm_learn_sgd
类,在fm_learn_sgd_element_adapt_reg
类中,最重要的函数为learn
函数,用于训练FM模型。函数的具体代码如下所示:
// self-adaptive-regularization 的训练
virtual void learn(Data& train, Data& test) {
fm_learn_sgd::learn(train, test);// 输出一些训练信息,继承自fm_learn_sgd类中的方法
std::cout << "Training using self-adaptive-regularization SGD."<< std::endl << "DON'T FORGET TO SHUFFLE THE ROWS IN TRAINING AND VALIDATION DATA TO GET THE BEST RESULTS." << std::endl;
// make sure that fm-parameters are initialized correctly (no other side effects)
// 确保初始化的过程
fm->w.init(0);
fm->reg0 = 0;
fm->regw = 0;
fm->regv = 0;
// start with no regularization
// 正则化参数的初始化,全部初始化为0
reg_w.init(0.0);
reg_v.init(0.0);
// 打印输出信息,包括训练样本点的条数和验证样本的条数
std::cout << "Using " << train.data->getNumRows() << " rows for training model parameters and " << validation->data->getNumRows() << " for training shrinkage." << std::endl;
// 基于梯度的训练过程
for (int i = 0; i < num_iter; i++) {// 开始每一轮的迭代
double iteration_time = getusertime();
// SGD-based learning: both lambda and theta are learned
// 分为lambda step和theta step
update_means();// 计算均值和方差
validation->data->begin();// 将验证集的指针指向开始位置
for (train.data->begin(); !train.data->end(); train.data->next()) {
// 计算theta相关,更新theta中的参数
// 利用训练集训练和更新模型的参数,此时模型中的正则化参数是固定的
sgd_theta_step(train.data->getRow(), train.target(train.data->getRowIndex()));
// 当i=0时,不需要更新lambda
if (i > 0) { // make no lambda steps in the first iteration, because some of the gradients (grad_theta) might not be initialized.
// 每次只使用validation中的一条样本
if (validation->data->end()) {
update_means();// 计算均值和方差
validation->data->begin();// 将验证集的指针指向开始位置
}
// 计算lambda相关,更新lambda中的参数
// 利用验证集更新正则化参数,此时模型中的参数是固定的
sgd_lambda_step(validation->data->getRow(), validation->target(validation->data->getRowIndex()));
validation->data->next();// 将验证集的指针指向下一条样本
}
}
// (3) Evaluation
iteration_time = (getusertime() - iteration_time);
// 评价函数
double rmse_val = evaluate(*validation);// 对验证集进行评测
double rmse_train = evaluate(train);// 对训练集进行评测
double rmse_test = evaluate(test);// 对测试集进行评测
// 打印输出模型的评估结果
std::cout << "#Iter=" << std::setw(3) << i << "\tTrain=" << rmse_train << "\tTest=" << rmse_test << std::endl;
// 日志输出
if (log != NULL) {
// log 输出均值和方差
log->log("wmean", mean_w);
log->log("wvar", var_w);
for (int f = 0; f < fm->num_factor; f++) {
{
std::ostringstream ss;
ss << "vmean" << f;
log->log(ss.str(), mean_v(f));
}
{
std::ostringstream ss;
ss << "vvar" << f;
log->log(ss.str(), var_v(f));
}
}
// log 输出正则化参数
for (uint g = 0; g < meta->num_attr_groups; g++) {
{
std::ostringstream ss;
ss << "regw[" << g << "]";
log->log(ss.str(), reg_w(g));
}
for (int f = 0; f < fm->num_factor; f++) {
{
std::ostringstream ss;
ss << "regv[" << g << "," << f << "]";
log->log(ss.str(), reg_v(g,f));
}
}
}
// log输出训练时间和评估效果
log->log("time_learn", iteration_time);
log->log("rmse_train", rmse_train);
log->log("rmse_val", rmse_val);
log->newLine();
}
}
}
根据上面的理论分析,Adaptive Regularization方法的学习过程分为两步:
- 固定正则化参数,利用训练集学习更新FM模型中的参数;
- 固定FM模型的参数,利用验证集学习更新正则化参数。
这两个过程可由下图表示:
在代码的实现过程中,sgd_theta_step
函数负责对FM模型中的参数进行学习和更新;sgd_lambda_step
函数负责对正则化参数进行学习和更新。sgd_theta_step
函数的具体代码如下所示:
// 计算theta相关,更新theta中的参数
void sgd_theta_step(sparse_row<FM_FLOAT>& x, const DATA_FLOAT target) {
double p = fm->predict(x, sum, sum_sqr);// 得到样本的预测值,在fm_model中
double mult = 0;
// 区分分类问题还是回归问题
if (task == 0) {
p = std::min(max_target, p);
p = std::max(min_target, p);
mult = 2 * (p - target);// 梯度值的一部分
} else if (task == 1) {
mult = target * ( (1.0/(1.0+exp(-target*p))) - 1.0 );// 梯度值的一部分
}
// make the update with my regularization constants:
// 更新每一部分的参数
// 1、更新常数项的权重
if (fm->k0) {
double& w0 = fm->w0;// 常数项的权重
double grad_0 = mult;// 梯度值
w0 -= learn_rate * (grad_0 + 2 * reg_0 * w0);// 更新常数项的权重
}
// 2、更新一次项的权重
if (fm->k1) {
for (uint i = 0; i < x.size; i++) {
uint g = meta->attr_group(x.data[i].id);// 取得参数对应的分组的编号
double& w = fm->w(x.data[i].id);// 得到模型的对应一次项的参数
grad_w(x.data[i].id) = mult * x.data[i].value;// 一次项的梯度值
w -= learn_rate * (grad_w(x.data[i].id) + 2 * reg_w(g) * w);// 更新一次项的权重值
}
}
// 3、更新交叉项的权重
for (int f = 0; f < fm->num_factor; f++) {
for (uint i = 0; i < x.size; i++) {
uint g = meta->attr_group(x.data[i].id);// 取得参数对应的分组的编号
double& v = fm->v(f,x.data[i].id);// 取得模型的对应交叉项的参数
grad_v(f,x.data[i].id) = mult * (x.data[i].value * (sum(f) - v * x.data[i].value)); // grad_v_if = (y(x)-y) * [ x_i*(\sum_j x_j v_jf) - v_if*x^2 ] // 交叉项的梯度值
v -= learn_rate * (grad_v(f,x.data[i].id) + 2 * reg_v(g,f) * v);// 更新交叉项的权重值
}
}
}
在计算的过程中,利用fm_model
类中的predict
函数计算当前的预测值,根据回归问题或者分类问题,计算其损失函数的梯度值,如回归时为:
并将2(y^(i)−y(i))作为变量mult
的值。分类时为:
并将(σ(y^(i)y(i))−1)⋅y(i)作为变量mult
的值。
在计算完变量mult
的值后,分别对常数项,一次项以及交叉项的参数利用梯度下降的方法进行更新,对于上述∂y^∂θ为:
利用梯度下降的方法更新的过程为:
在libFM的实现过程中,对正则化参数进行了分组,分组的概念如下图所示:
假设有7个参数,标号分别为{0,1,2,3,4,5,6,7},假设将0和1划分到一个分组中,如下图中的2标号,同理,将2,6划分到一个分组,将3,4,5划分到一个分组中,对于同一个分组,其拥有一个单独的正则化参数。这样就能够减少正则化参数的个数。
第二个重要的部分是sgd_lambda_step
函数,其具体的代码如下所示:
// 计算lambda相关,更新lambda中的参数
void sgd_lambda_step(sparse_row<FM_FLOAT>& x, const DATA_FLOAT target) {
double p = predict_scaled(x);// 扩展后的预测值
double grad_loss = 0;
// 区分两类问题:回归问题和分类问题
if (task == 0) {// 回归问题
p = std::min(max_target, p);
p = std::max(min_target, p);
grad_loss = 2 * (p - target);
} else if (task == 1) {// 分类问题
grad_loss = target * ( (1.0/(1.0+exp(-target*p))) - 1.0);
}
// 1、更新一次项的正则化参数
if (fm->k1) {
lambda_w_grad.init(0.0);// 初始化
// 将累加和分配到每一个分组中
for (uint i = 0; i < x.size; i++) {
uint g = meta->attr_group(x.data[i].id);// 取得当前特征对应的正则化参数的索引
lambda_w_grad(g) += x.data[i].value * fm->w(x.data[i].id);// 在对应的分组中计算累加和
}
// 修改每一个分组内的正则化参数
for (uint g = 0; g < meta->num_attr_groups; g++) {
lambda_w_grad(g) = -2 * learn_rate * lambda_w_grad(g);
reg_w(g) -= learn_rate * grad_loss * lambda_w_grad(g);
reg_w(g) = std::max(0.0, reg_w(g));// 对修改后的正则化参数容错,防止其小于0
}
}
// 2、更新交叉项的正则化参数
for (int f = 0; f < fm->num_factor; f++) {
// grad_lambdafg = (grad l(y(x),y)) * (-2 * alpha * (\sum_{l} x_l * v'_lf) * (\sum_{l \in group(g)} x_l * v_lf) - \sum_{l \in group(g)} x^2_l * v_lf * v'_lf)
// sum_f_dash := \sum_{l} x_l * v'_lf, this is independent of the groups
// sum_f(g) := \sum_{l \in group(g)} x_l * v_lf
// sum_f_dash_f(g) := \sum_{l \in group(g)} x^2_l * v_lf * v'_lf
double sum_f_dash = 0.0;
sum_f.init(0.0);
sum_f_dash_f.init(0.0);
for (uint i = 0; i < x.size; i++) {
// v_if' = [ v_if * (1-alpha*lambda_v_f) - alpha * grad_v_if]
uint g = meta->attr_group(x.data[i].id);// 取得当前特征对应的正则化参数的索引
double& v = fm->v(f,x.data[i].id);// 取得模型的对应交叉项的参数
double v_dash = v - learn_rate * (grad_v(f,x.data[i].id) + 2 * reg_v(g,f) * v);
// 更新公式中的三项
sum_f_dash += v_dash * x.data[i].value;
sum_f(g) += v * x.data[i].value;
sum_f_dash_f(g) += v_dash * x.data[i].value * v * x.data[i].value;
}
// 对每一个分组中的正则化参数更新
for (uint g = 0; g < meta->num_attr_groups; g++) {
double lambda_v_grad = -2 * learn_rate * (sum_f_dash * sum_f(g) - sum_f_dash_f(g));
reg_v(g,f) -= learn_rate * grad_loss * lambda_v_grad;
reg_v(g,f) = std::max(0.0, reg_v(g,f));// 对修改后的正则化参数容错,防止其小于0
}
}
}
sgd_lambda_step
函数是在固定FM模型参数的前提下,利用验证数据集对每个分组中的正则化参数进行学习和更新。在这个过程中,首先是需要计算扩展后的预测值,即在利用更新后的FM模型的参数进行预测:
其具体的计算过程如predict_scaled
函数所示:
// 扩展后的预测值,是指在模型的预测值中增加了正则项
double predict_scaled(sparse_row<FM_FLOAT>& x) {
double p = 0.0;// 最终的预测值
// 1、常数项
if (fm->k0) {
p += fm->w0;// 常数项,注意这边并没有对常数项增加正则
}
// 2、一次项
if (fm->k1) {
// 累加每一维特征项
for (uint i = 0; i < x.size; i++) {
assert(x.data[i].id < fm->num_attribute);// 特征的维度的容错
uint g = meta->attr_group(x.data[i].id);// 取得当前特征对应的正则化参数的索引
double& w = fm->w(x.data[i].id);// 取得当前的权重
double w_dash = w - learn_rate * (grad_w(x.data[i].id) + 2 * reg_w(g) * w);// 更新权重
p += w_dash * x.data[i].value; // 累加计算
}
}
// 3、交叉项
for (int f = 0; f < fm->num_factor; f++) {
// sum和sum_sqr分别对应着交叉项计算中的两项
sum(f) = 0.0;
sum_sqr(f) = 0.0;
for (uint i = 0; i < x.size; i++) {
uint g = meta->attr_group(x.data[i].id);// 取得当前特征对应的正则化参数的索引
double& v = fm->v(f,x.data[i].id);// 取得模型的对应交叉项的参数
double v_dash = v - learn_rate * (grad_v(f,x.data[i].id) + 2 * reg_v(g,f) * v);// 更新交叉项的参数
double d = v_dash * x.data[i].value;
sum(f) += d;
sum_sqr(f) += d*d;
}
p += 0.5 * (sum(f)*sum(f) - sum_sqr(f));
}
return p;
}
注意:在libFM的实现中,并没有对模型的常数项增加正则项,因此在更新正则项参数时也不需要更新常数项的正则化参数。
对预测值的计算,可以参见“机器学习算法实现解析——libFM之libFM的模型处理部分”。
计算完扩展的预测值后,便开始计算损失函数对正则化参数的梯度,并对其进行更新,更新的详细步骤参见“5.3.2、Adaptive Regularization方法的理论”中的讲解。
除了上述的重要的过程外,在fm_learn_sgd_element_adapt_reg
类中还提供了如下的几个函数:
- 初始化
init
函数
// 初始化函数,比SGD中初始化更多的参数
virtual void init() {
fm_learn_sgd::init();
reg_0 = 0;// 常数项的正则化参数的初始化
reg_w.setSize(meta->num_attr_groups);// 一次项的正则化参数
reg_v.setSize(meta->num_attr_groups, fm->num_factor);// 交叉项的正则化参数
// 交叉项的均值和方差
mean_v.setSize(fm->num_factor);
var_v.setSize(fm->num_factor);
// 一次项的梯度的初始化
grad_w.setSize(fm->num_attribute);
// 交叉项的梯度的初始化
grad_v.setSize(fm->num_factor, fm->num_attribute);
grad_w.init(0.0);
grad_v.init(0.0);
lambda_w_grad.setSize(meta->num_attr_groups);// 正则化参数的梯度
// 更新lambda时使用到的变量
sum_f.setSize(meta->num_attr_groups);
sum_f_dash_f.setSize(meta->num_attr_groups);
// 日志文件
if (log != NULL) {
log->addField("rmse_train", std::numeric_limits<double>::quiet_NaN());
log->addField("rmse_val", std::numeric_limits<double>::quiet_NaN());
log->addField("wmean", std::numeric_limits<double>::quiet_NaN());
log->addField("wvar", std::numeric_limits<double>::quiet_NaN());
for (int f = 0; f < fm->num_factor; f++) {
{
std::ostringstream ss;
ss << "vmean" << f;
log->addField(ss.str(), std::numeric_limits<double>::quiet_NaN());
}
{
std::ostringstream ss;
ss << "vvar" << f;
log->addField(ss.str(), std::numeric_limits<double>::quiet_NaN());
}
}
for (uint g = 0; g < meta->num_attr_groups; g++) {
{
std::ostringstream ss;
ss << "regw[" << g << "]";
log->addField(ss.str(), std::numeric_limits<double>::quiet_NaN());
}
for (int f = 0; f < fm->num_factor; f++) {
{
std::ostringstream ss;
ss << "regv[" << g << "," << f << "]";
log->addField(ss.str(), std::numeric_limits<double>::quiet_NaN());
}
}
}
}
}
init
函数主要用于对一些变量的初始化。
update_means
函数
// 初始化相关
void update_means() {
// 均值和方差
mean_w = 0;
mean_v.init(0);
var_w = 0;
var_v.init(0);
// 1、计算w的均值和方差
for (uint j = 0; j < fm->num_attribute; j++) {
mean_w += fm->w(j);
var_w += fm->w(j)*fm->w(j);
for (int f = 0; f < fm->num_factor; f++) {
mean_v(f) += fm->v(f,j);
var_v(f) += fm->v(f,j)*fm->v(f,j);
}
}
mean_w /= (double) fm->num_attribute;// 计算均值
var_w = var_w/fm->num_attribute - mean_w*mean_w;// 计算方差
// 2、计算v的均值和方差
for (int f = 0; f < fm->num_factor; f++) {
mean_v(f) /= fm->num_attribute;
var_v(f) = var_v(f)/fm->num_attribute - mean_v(f)*mean_v(f);
}
// 3、重新置均值为0
mean_w = 0;
for (int f = 0; f < fm->num_factor; f++) {
mean_v(f) = 0;
}
}
update_means
函数用于对模型的参数求方差,这整个训练过程中并不起作用,只是为了log输出作为参考。
debug
函数
// debug打印输出
void debug() {
std::cout << "method=sgda" << std::endl;
fm_learn_sgd::debug();
}
参考文献
- Rendle S. Factorization Machines[C]// IEEE International Conference on Data Mining. IEEE Computer Society, 2010:995-1000.
- Rendle S. Factorization Machines with libFM[M]. ACM, 2012.
- Rendle S. Learning recommender systems with adaptive regularization[C]// ACM International Conference on Web Search and Data Mining. ACM, 2012:133-142.
机器学习算法实现解析——libFM之libFM的训练过程之Adaptive Regularization的更多相关文章
- 机器学习算法实现解析——libFM之libFM的训练过程之SGD的方法
本节主要介绍的是libFM源码分析的第五部分之一--libFM的训练过程之SGD的方法. 5.1.基于梯度的模型训练方法 在libFM中,提供了两大类的模型训练方法,一类是基于梯度的训练方法,另一类是 ...
- 机器学习算法实现解析——libFM之libFM的训练过程概述
本节主要介绍的是libFM源码分析的第四部分--libFM的训练. FM模型的训练是FM模型的核心的部分. 4.1.libFM中训练过程的实现 在FM模型的训练过程中,libFM源码中共提供了四种训练 ...
- 机器学习算法实现解析——libFM之libFM的模型处理部分
本节主要介绍的是libFM源码分析的第三部分--libFM的模型处理. 3.1.libFM中FM模型的定义 libFM模型的定义过程中主要包括模型中参数的设置及其初始化,利用模型对样本进行预测.在li ...
- 机器学习算法实现解析——word2vec源代码解析
在阅读本文之前,建议首先阅读"简单易学的机器学习算法--word2vec的算法原理"(眼下还没公布).掌握例如以下的几个概念: 什么是统计语言模型 神经概率语言模型的网络结构 CB ...
- 机器学习算法与Python实践之(四)支持向量机(SVM)实现
机器学习算法与Python实践之(四)支持向量机(SVM)实现 机器学习算法与Python实践之(四)支持向量机(SVM)实现 zouxy09@qq.com http://blog.csdn.net/ ...
- 机器学习算法与Python实践之(五)k均值聚类(k-means)
机器学习算法与Python实践这个系列主要是参考<机器学习实战>这本书.因为自己想学习Python,然后也想对一些机器学习算法加深下了解,所以就想通过Python来实现几个比较常用的机器学 ...
- 机器学习算法与Python实践之(七)逻辑回归(Logistic Regression)
http://blog.csdn.net/zouxy09/article/details/20319673 机器学习算法与Python实践之(七)逻辑回归(Logistic Regression) z ...
- 机器学习算法( 五、Logistic回归算法)
一.概述 这会是激动人心的一章,因为我们将首次接触到最优化算法.仔细想想就会发现,其实我们日常生活中遇到过很多最优化问题,比如如何在最短时间内从A点到达B点?如何投入最少工作量却获得最大的效益?如何设 ...
- 机器学习算法( 二、K - 近邻算法)
一.概述 k-近邻算法采用测量不同特征值之间的距离方法进行分类. 工作原理:首先有一个样本数据集合(训练样本集),并且样本数据集合中每条数据都存在标签(分类),即我们知道样本数据中每一条数据与所属分类 ...
随机推荐
- 记一次redis key丢失的问题排查
最近测试环境的redis经常性发生某些key丢失的问题,最终的找到的问题让人大吃一惊. 复盘一下步骤: 1.发现问题 不知道从某天开始,后台经常报错,原因是某些key丢失,一开始不在意,以为是小bug ...
- JavaScript 数据类型小结
数据类型对于机器而言,其意义在于更加合理的分配内存空间,而对于编程者而言,数据类型提供了我们相对应的一系列方法,对数据进行分析与处理. 在本文中,将对JavaScript数据类型的基础知识进行总结,全 ...
- JAVA基础补漏--SET
HashSet: 1.无序集合. 2.底层是一个哈希表结构,查询速速很快. 哈希表==数据 + 链表/红黑树 特点:查询速度快. 存储数据到SET中: 1.计算数据的HASH值. 2.查看有没有相同H ...
- 【cs231n】卷积神经网络
较好的讲解博客: 卷积神经网络基础 深度卷积模型 目标检测 人脸识别与神经风格迁移 译者注:本文翻译自斯坦福CS231n课程笔记ConvNet notes,由课程教师Andrej Karpathy授权 ...
- C# winfrom listview 多窗口调用
Form1 private void button1_Click(object sender, EventArgs e) { Form f = new Form2(ref listView1); f. ...
- hdu 1241 搬寝室 水dp
搬寝室 Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others) Problem Desc ...
- nodejs 备忘
引入模块(在于你用什么模块,需要的模块可以用终端进行安装, npm,一般express,swig,body-parser,cookies,markdown) 设置模块 设置渲染 var express ...
- Spark 基于物品的协同过滤算法实现
J由于 Spark MLlib 中协同过滤算法只提供了基于模型的协同过滤算法,在网上也没有找到有很好的实现,所以尝试自己实现基于物品的协同过滤算法(使用余弦相似度距离) 算法介绍 基于物品的协同过滤算 ...
- angularjs1 自定义图片查看器(可旋转、放大、缩小、拖拽)
笔记: angularjs1 制作自定义图片查看器(可旋转.放大.缩小.拖拽) 2018-01-12 更新 可以在我的博客 查看我 已经封装好的 纯 js写的图片查看器插件 博客链接 懒得把 ...
- 【Python】关于使用pycharm遇到只能使用unittest方式运行,无法直接选择Run
相信大家可能都遇到过这个问题,使用pycharm直接运行脚本的时候,只能选择unittest的方式,能愁死个人