为什么上一篇还是三,这一篇就跳到五了呢?其实我们原来提到过:
这里的模型与算法我们之前都已经提到过了,虽然只是介绍了一个基类,并没有涉及到其具体的实现。在这里我们就会揭开其真正面目了。『策略』我们还没有介绍过,其实就是目标函数,在前面一些较为简单的算法中并没有涉及到这块。为了整个逻辑的完整性,我还是打算将其放在前面来介绍。
这里我们所介绍的算法是线性回归算法。它是机器学习算法中非常基本的一个算法。这里就不对它进行过多的介绍了,之后应该会写一个博客来叙述。
首先给出一个示例代码,使得有一个整体的映像。
#include
#include
#include
#include
using namespace shark;
using namespace std;
int main(int argc, char **argv) {
if(argc < 3) {
cerr << "usage: " << argv[0] << " (file with inputs/independent variables) (file with outputs/dependent variables)" << endl;
exit(EXIT_FAILURE);
}
Data inputs;
Data labels;
try {
importCSV(inputs, argv[1], ' ');
}
catch (...) {
cerr << "unable to read input data from file " << argv[1] << endl;
exit(EXIT_FAILURE);
}
try {
importCSV(labels, argv[2]);
}
catch (...) {
cerr << "unable to read labels from file " << argv[2] << endl;
exit(EXIT_FAILURE);
}
RegressionDataset data(inputs, labels);
// trainer and model
LinearRegression trainer;
LinearModel<> model;
// train model
trainer.train(model, data);
// show model parameters
cout << "intercept: " << model.offset() << endl;
cout << "matrix: " << model.matrix() << endl;
SquaredLoss<> loss;
Data prediction = model(data.inputs());
cout << "squared loss: " << loss(data.labels(), prediction) << endl;
}
首先读取算法所需要的数据集,这里是存储在LabeledData所特化的RegressionDataset中。之后就是初始化算法所对应的模型类,以及算法的训练方法类。利用训练方法类对训练数据进行训练,将训练所得的参数写回到对应的模型中。这里的prediction就是对于数据的预测值。模型重载了括号运算符,里面包含的内容是eval函数,就是计算其输出值。最后利用了平方损失函数来衡量模型的性能。
Shark中将线性回归算法归于线性模型这一大类中。线性模型是使用线性函数 f(x)=Ax+b 来进行预测的。存在两个特殊的情况是:一是输出可能只是一个单独的数;二是,偏移b可能会被省略。
该文件位于
中。
template <class InputType = RealVector>
class LinearModel : public AbstractModel<InputType,RealVector>
{
private:
typedef AbstractModel<InputType,RealVector> base_type;
typedef LinearModel<InputType> self_type;
RealMatrix m_matrix; // 权值矩阵
RealVector m_offset; // 偏置向量
public:
typedef typename base_type::BatchInputType BatchInputType;
typedef typename base_type::BatchOutputType BatchOutputType;
LinearModel(){
base_type::m_features |= base_type::HAS_FIRST_PARAMETER_DERIVATIVE;
base_type::m_features |= base_type::HAS_FIRST_INPUT_DERIVATIVE;
}
LinearModel(std::size_t inputs, std::size_t outputs = 1, bool offset = false)
: m_matrix(outputs,inputs,0.0),m_offset(offset?outputs:0,0.0){
base_type::m_features |= base_type::HAS_FIRST_PARAMETER_DERIVATIVE;
base_type::m_features |= base_type::HAS_FIRST_INPUT_DERIVATIVE;
}
LinearModel(LinearModel const& model)
:m_matrix(model.m_matrix),m_offset(model.m_offset){
base_type::m_features |= base_type::HAS_FIRST_PARAMETER_DERIVATIVE;
base_type::m_features |= base_type::HAS_FIRST_INPUT_DERIVATIVE;
}
std::string name() const
{ return "LinearModel"; }
friend void swap(LinearModel& model1,LinearModel& model2){
swap(model1.m_matrix,model2.m_matrix);
swap(model1.m_offset,model2.m_offset);
}
LinearModel& operator=(LinearModel const& model){
self_type tempModel(model);
swap(*this,tempModel);
return *this;
}
LinearModel(RealMatrix const& matrix, RealVector const& offset = RealVector())
:m_matrix(matrix),m_offset(offset){
base_type::m_features |= base_type::HAS_FIRST_PARAMETER_DERIVATIVE;
base_type::m_features |= base_type::HAS_FIRST_INPUT_DERIVATIVE;
}
// 返回该模型是否需要偏置
bool hasOffset() const{
return m_offset.size() != 0;
}
// 返回输入的维度,这里的size2函数输出的是矩阵的列数
size_t inputSize() const{
return m_matrix.size2();
}
// 返回输出的维度,这里的size1函数输出的是矩阵的行数,
// 输出的应该是输入数据的预测值,所以这里应该是一一对应的关系
size_t outputSize() const{
return m_matrix.size1();
}
// 将所有的参数向量化输出
RealVector parameterVector() const{
RealVector ret(numberOfParameters());
init(ret) << toVector(m_matrix),m_offset;
return ret;
}
// 根据输入的向量来修改参数的值
void setParameterVector(RealVector const& newParameters)
{
init(newParameters) >> toVector(m_matrix),m_offset;
}
size_t numberOfParameters() const{
return m_matrix.size1()*m_matrix.size2()+m_offset.size();
}
//可以通过该函数的不同重载版本设置输入输出的规模,乃至参数矩阵,offset的需要与否
void setStructure(std::size_t inputs, std::size_t outputs = 1, bool offset = false){
LinearModel<InputType> model(inputs,outputs,offset);
swap(*this,model);
}
void setStructure(RealMatrix const& matrix, RealVector const& offset = RealVector()){
m_matrix = matrix;
m_offset = offset;
}
RealMatrix const& matrix() const{
return m_matrix;
}
RealMatrix& matrix(){
return m_matrix;
}
RealVector const& offset() const{
return m_offset;
}
RealVector& offset(){
return m_offset;
}
boost::shared_ptr<State> createState()const{
return boost::shared_ptr<State>(new EmptyState());
}
using base_type::eval;
//计算模型的输出值
void eval(BatchInputType const& inputs, BatchOutputType& outputs)const{
outputs.resize(inputs.size1(),m_matrix.size1());
noalias(outputs) = prod(inputs,trans(m_matrix));
if (hasOffset()){
noalias(outputs)+=repeat(m_offset,inputs.size1());
}
}
void eval(BatchInputType const& inputs, BatchOutputType& outputs, State& state)const{
eval(inputs,outputs);
}
//以下这两个函数的使用可以在之后的前反馈神经网络算法中看到其非常典型的使用
//这里的线性回归算法的训练方法使用的是最小二乘法,所以这里还用不到梯度的计算
void weightedParameterDerivative(
BatchInputType const& patterns, RealMatrix const& coefficients, State const& state, RealVector& gradient
)const{
SIZE_CHECK(coefficients.size2()==outputSize());
SIZE_CHECK(coefficients.size1()==patterns.size1());
gradient.resize(numberOfParameters());
std::size_t inputs = inputSize();
std::size_t outputs = outputSize();
gradient.clear();
blas::dense_matrix_adaptor weightGradient = blas::adapt_matrix(outputs,inputs,gradient.storage());
//noalias是取右值
noalias(weightGradient) = prod(trans(coefficients),patterns);
if (hasOffset()){
std::size_t start = inputs*outputs;
noalias(subrange(gradient, start, start + outputs)) = sum_rows(coefficients);
}
}
void weightedInputDerivative(
BatchInputType const & patterns,
BatchOutputType const & coefficients,
State const& state,
BatchInputType& derivative
)const{
SIZE_CHECK(coefficients.size2() == outputSize());
SIZE_CHECK(coefficients.size1() == patterns.size1());
derivative.resize(patterns.size1(),inputSize());
noalias(derivative) = prod(coefficients,m_matrix);
}
void read(InArchive& archive){
archive >> m_matrix;
archive >> m_offset;
}
void write(OutArchive& archive) const{
archive << m_matrix;
archive << m_offset;
}
};
之前介绍完了模型,现在就要来说说它的学习算法了。之前也提到了,所使用的学习算法是最小二乘法,这个会在之后的博客中提及,这里就不再赘述了。
该类定义在
文件中,其实现在
中,这里我将两者的代码合并在一起。
class LinearRegression : public AbstractTrainer<LinearModel<> >, public IParameterizable
{
public:
SHARK_EXPORT_SYMBOL LinearRegression(double regularization = 0.0);
std::string name() const
{ return "LinearRegression"; }
double regularization() const{
return m_regularization;
}
void setRegularization(double regularization) {
RANGE_CHECK(regularization >= 0.0);
m_regularization = regularization;
}
RealVector parameterVector() const {
RealVector param(1);
param(0) = m_regularization;
return param;
}
void setParameterVector(const RealVector& param) {
SIZE_CHECK(param.size() == 1);
m_regularization = param(0);
}
size_t numberOfParameters() const{
return 1;
}
void LinearRegression::train(LinearModel<>& model, LabeledData<RealVector, RealVector> const& dataset){
//虽然知道这里使用的是最小二乘法,但是一些具体的数据结构我还是有点看不太懂,这里我只能说个大概
std::size_t inputDim = inputDimension(dataset);
std::size_t outputDim = labelDimension(dataset);
std::size_t numInputs = dataset.numberOfElements();
std::size_t numBatches = dataset.numberOfBatches();
//这里多出来的一维是给offset准备的
RealMatrix matA(inputDim+1,inputDim+1,0.0);
blas::Blocking<RealMatrix> Ablocks(matA,inputDim,inputDim);
typedef LabeledData<RealVector, RealVector>::const_batch_reference BatchRef;
//计算x^Tx
for (std::size_t b=0; b != numBatches; b++){
BatchRef batch = dataset.batch(b);
//计算xx^T
symm_prod(trans(batch.input),Ablocks.upperLeft(),false);
noalias(column(Ablocks.upperRight(),0))+=sum_rows(batch.input);
}
row(Ablocks.lowerLeft(),0) = column(Ablocks.upperRight(),0);
matA(inputDim,inputDim) = numInputs;
diag(Ablocks.upperLeft())+= m_regularization;
//计算x^T * y
RealMatrix XTL(inputDim + 1,outputDim,0.0);
for (std::size_t b=0; b != numBatches; b++){
BatchRef batch = dataset.batch(b);
RealSubMatrix PTL = subrange(XTL,0,inputDim,0,outputDim);
noalias(PTL) += prod(trans(batch.input),batch.label);
noalias(row(XTL,inputDim))+=sum_rows(batch.label);
}
//这里相当于是解一个方程(x^Tx)w = x^T * y,来求得w
blas::solveSymmSemiDefiniteSystemInPlaceSolveAXB >(matA,beta);
RealMatrix matrix = subrange(trans(beta), 0, outputDim, 0, inputDim);
RealVector offset = row(beta,inputDim);
// 将训练所得的参数写回到对应的模型中去
model.setStructure(matrix, offset);
}
protected:
// 这个参数我也不知道有什么用,我在开发者邮件列表中问过,但是没有人回答我
double m_regularization;
};
Lasso回归算法其实是线性回归算法的一种特殊形式。它是在原有目标函数的基础之上加入了1-范数,所以目标函数的形式如下:
其中1-范数的作用就是使得学习出来的权值向量中的非零值更少,即代表着只有少数的特征对输出起着重要的作用。
当然,也可以将1-范数替换为2-范数,这样的方法被称为『岭回归』(ridge regression)。通过该算法所学习到的特征会更短。这个问题在我面试腾讯的时候有被问到过。
这两种方法都有助于降低过拟合的风险。在《机器学习》(周志华)一书中,还将其作为一种特征选择的方法。
既然Lasso回归只是线性回归的一个变种,它们当然都归属于线性模型。传统的解决Lasso回归的方法是坐标下降算法,但是Shark中并没有使用该方法。所以我就没有看,感兴趣的话可以去看看。
这里就简单地说说坐标下降算法。坐标下降算法:为了找到一个函数的局部极小值,在每次迭代中可以在当前点处沿一个坐标方向进行一维搜索。在整个过程中循环使用不同的坐标方向。一个周期的一维搜索迭代过程相当于一个梯度迭代。如果每一步的计算都比较简单,那么算法也会比较有效,同时也能够缓解算法没有考虑到变量之间关联性的问题。算法的具体步骤如下。
Input: x(0)∈Rn
t = 0
repeat
选取一个活动的维度 i(t)∈ {1,…,n}
根据约束 x(t)j=x(t−1)j,j≠i(t) ,解决最优化问题
t = t + 1
until 达到退出条件
这里的 i(t) 的选择可以以循环的方式来选择(按照维度的顺序依次被选择),或是随机地选择。