Liblinear-thư viện học máy


Khái quát về Liblinear

Liblinear là 1 thư viện các công cụ sử dụng trong học máy, do nhóm nghiên cứu thuộc trường đại học Taiwan phát triển.
Trang chủ tiếng Anh tại đây.

Bài viết này sẽ cố gắng dịch, giải thích và 1 số ghi chú trong quá trình sử dụng Liblinear của cá nhân tôi.
Tại thời điểm hiện tại, tôi viết bài này cho Liblinear bản mới nhất là Liblinear 1.9. Tùy theo phiên bản, sẽ có 1 số điểm khác nhau. Ví dụ như bản 1.8 và 1.9 hàm predict trả lại định dạng khác nhau : int với bản 1.8 và double với bản 1.9.

Các cấu trúc dữ liệu cơ bản

struct feature_node

{
    int index;
    double value;
};

  • index là chỉ số của các feature (đặc trưng). Trong Liblinear, các đặc trưng đều được biểu diễn dưới dạng số, vì thế khi sử dụng Liblinear, bạn cần 1 class quản lý các đặc trưng cùng các chỉ số của nó. Khi bạn muốn lưu lại các model, bạn cũng phải lưu lại cả các đặc trưng và chỉ số đó.
    Các chỉ số phải là số nguyên, bắt đầu từ 1, và phải liên tiếp. Nghĩa là, chỉ số phụ thuộc vào thứ tự xuất hiện của đặc trưng trong dữ liệu huấn luyện. Bạn không thể chỉ định trước các chỉ số này.
    Điều này cũng tương tự với các label của các class mà ta sẽ nói tới sau đây.
  • value là giá trị của các đặc trưng. Với bài toán xử lý ngôn ngữ tự nhiên, thường các value này là tần số xuất hiện của đặc trưng đó.

struct problem

{
    int l, n;
    double *y;
    struct feature_node **x;
    double bias;            /* < 0 if no bias term */ 
};

  • l (L) là số lượng dữ liệu huấn luyện.
  • n là số chủng loại đặc trưng. (với cách định nghĩa của feature_node, n cũng chính là chỉ số lớn nhất trong tập các đặc trưng)
  • bias(tôi cũng không thực sự hiểu rõ về giá trị này, tôi sẽ viết tiếp khi có thể).
  • y là 1 mảng 1 chiều lưu lại chỉ số của các class. Kích thước của y chính bằng giá trị l.
  • x là 1 mảng 2 chiều, có kích thước 1 chiều cố định là l, chiều kia thay đổi tùy theo dữ liệu, lưu lại các chỉ số của các feature. Các chỉ số này phải được sắp xếp theo thứ tự tăng dần.

struct parameter

{
    int solver_type;
    /* these are for training only */
    double eps;            /* stopping criteria */
    double C;
    int nr_weight;
    int *weight_label;
    double* weight;
    double p;
};

enum { L2R_LR, L2R_L2LOSS_SVC_DUAL, L2R_L2LOSS_SVC, L2R_L1LOSS_SVC_DUAL, MCSVM_CS, L1R_L2LOSS_SVC, L1R_LR, L2R_LR_DUAL, L2R_L2LOSS_SVR = 11, L2R_L2LOSS_SVR_DUAL, L2R_L1LOSS_SVR_DUAL }; /* solver_type */

Những giá trị trong parameter sẽ ảnh hưởng trực tiếp tới kết quả của quá trình phân loại. parameter chỉ đơn giản là nhập trực tiếp bằng tay các giá trị vào, vì thế chỉ cần xây dựng được problem thì có thể huấn luyện được ngay. parameter thường được khai báo const ngay từ đầu chương trình.
  • solver_type : dạng máy phân loại. 1 số dạng :
    • L1R_L2LOSS_SVC  : L1-regularized L2-loss support vector classification
    • L1R_LR : L1-regularized logistic regression
    • L2R_LR : L2-regularized logistic regression (primal) (fka L2_LR)
    • MCSVM_CS : multi-class support vector classification by Crammer and Singer
  • C : là chi phí của các vi phạm. Thường được đặt là 1.0
  • eps : tiêu chí dừng. Hay hiểu cách khác là giới hạn sai số cho phép. Thường được đặt là 0.01

struct model

{
    struct parameter param;
    int nr_class;        /* number of classes */
    int nr_feature;
    double *w;
    int *label;        /* label of each class */
    double bias;
};
Liblinear cung cấp 2 hàm
int save_model(const char *model_file_name, const struct model *model_);

để lưu các model, và
struct model *load_model(const char *model_file_name);

để đọc các model từ file.
Bạn không cần thiết phải hiểu quá rõ về cấu trúc model, vì thực tế bạn sẽ chỉ thao tác chủ yếu trên cấu trúc problem và feature_node, từ đó sử dụng các hàm có sẵn trong Liblinear để nhận lại model. Các model này ta sẽ chỉ sử dụng pointer để bê nó vào phần tham số của các hàm mà thôi.
  • w là mảng trọng số của các đặc trưng. w có kích thước là nr_feature * nr_class
         +------------------+------------------+------------+
         | nr_class weights | nr_class weights |  ...
         | for 1st feature  | for 2nd feature  |
         +------------------+------------------+------------+

Các thao tác cơ bản trên Liblinear

train

struct model* train(const struct problem *prob, const struct parameter *param);
Thao tác thực hiện việc huấn luyện dữ liệu.
Dữ liệu được thay đổi kiểu thành problem cho phù hợp với Liblinear và đưa vào thông qua con trỏ. Tương tự với parameter.
Sau khi huấn luyện, Liblinear sẽ trả lại 1 con trỏ chỉ đến model thu được.

cross_validation

void cross_validation(const struct problem *prob, const struct parameter *param, int nr_fold, double *target);

predict

double predict(const struct model *model_, const struct feature_node *x);
Hàm thực hiện việc phân loại.
Tham số là model và tập các đặc trưng feature_node.
Hàm trả lại 1 giá trị dạng double, là 1 giá trị trong tập các nhãn của model.

predict_probability

double predict_probability(const struct model *model_, const struct feature_node *x, double* prob_estimates);


save_model

int save_model(const char *model_file_name, const struct model *model_);
Lưu lại model vào 1 file.

load_model

struct model *load_model(const char *model_file_name);
Load model từ 1 file.

get_nr_feature

int get_nr_feature(const struct model *model_);


get_nr_class

int get_nr_class(const struct model *model_);


get_labels

void get_labels(const struct model *model_, int* label);
Lấy ra tập hợp các nhãn label trong model.

free_model_content

void free_model_content(struct model *model_ptr);
Xóa các dữ liệu chứa bên trong model.

free_and_destroy_model

void free_and_destroy_model(struct model **model_ptr_ptr);
Xóa và giải phóng bộ nhớ với cấu trúc dữ liệu model.

destroy_param

void destroy_param(struct parameter *param);
Xóa và giải phóng bộ nhớ với cấu trúc dữ liệu parameter.

Cách sử dụng Liblinear

Có 2 cách cơ bản để sử dụng Liblinear cũng như Libsvm là sử dụng như 1 chương trình riêng biệt, hoặc sử dụng như 1 thư viện.

Sử dụng như một thư viện

Giả sử ta có 1 project tên là Sample như sau :
Sample
|------------sample.cpp
Để sử dụng thư viện Liblinear, ta copy các file : linear.cpp, linear.h, tron.cpp, tron.h, thư mục blas vào dưới thư mục Sample. Ta sẽ được dạng sau :
Sample
|-------------blas
|-------------linear.cpp
|-------------linear.h
|-------------tron.cpp
|-------------tron.h
|-------------sample.cpp
Trong phần header của sample.cpp, ta khai báo thêm
#include "linear.h"
Sau đó ta có thể sử dụng các hàm xây dựng sẵn trong Liblinear mà tôi đã liệt kê ở trên.

Sử dụng như 1 chương trình riêng

Bạn chỉ cần cài đặt giống như những chương trình linux miễn phí khác bằng lệnh configure và lệnh make.
Liblinear có giao diện dòng lệnh cơ bản và không quá phức tạp. Bạn có thể tìm thấy trong phần hướng dẫn về Liblinear trên trang chủ.
Để tạo ra các file có định dạng chuẩn cho Liblinear và Libsvm, bạn tham khảo thêm phần "quản lý problem".

Một số đoạn code có ích

Dù Liblinear đã dựng sẵn cho ta 1 số thao tác cơ bản và cấu trúc dữ liệu, nhưng ta vẫn nên chỉnh sửa lại 1 chút để dễ dàng sử dụng và phát triển hơn. Ứng với mỗi cấu trúc dữ liệu cơ bản, tôi thường sử dụng 1 class tương ứng và các thao tác trên dữ liệu đó.

Vì những thao tác này cũng không quá phức tạp nên tôi sẽ nhóm từng class vào 1 file .h chứ không để .h&.cpp.

Quản lý và phân phối các chỉ số cho các feature và label

/* Debug : Done!
 * class-StrMap.h
 *
 *  Created on: 2012/04/20
 *  Author: LUU TUAN ANH - anh@jnlp.org
 */

#ifndef CLASS_STRMAP_H_
#define CLASS_STRMAP_H_
#include <map>
#include <string>
using namespace std;

class StrMap {
    private:
        map<string, int> smap_;
        int size_;
        bool isfull_;
    public:
        StrMap() : smap_(), size_(1), isfull_(false){}
        ~StrMap() {}

        int GetNum(const string str) {
            map<string, int>::iterator it = smap_.find(str);
            if (it == smap_.end()) {
                if (!isfull_) {
                    unsigned int num = size_;
                    if (num == INT_MAX) isfull_ = true;
                    smap_.insert(pair<string, int>(str, num));
                    size_ ++;
                    return num;
                }
            } else {
                return it->second;
            }
            return -1;
        }

        int size() const {return size_ - 1;}
        bool isfull() {return isfull_;}
        map<string, int>::iterator begin() {return smap_.begin(); }
        map<string, int>::iterator end() {return smap_.end(); }
        

        void insert(pair<string, int> pa) {
            smap_.insert(pa);
            size_ ++;
        }
};

  • Trên đây, tôi giới hạn số lượng feature là INT_MAX. Nếu bạn cần 1 lượng features lớn hơn, chỉ cần thay đổi giới hạn đó sao cho phù hợp là được.
  • StrMap::insert có vẻ không cần thiết, nhưng khi bạn load dữ liệu từ file, thì thao tác đó sẽ cần thiết.
  • StrMap::GetNum : khi bạn đưa vào 1 đặc trưng, chương trình sẽ kiểm tra xem đặc trưng đó đã tồn tại hay chưa. Nếu đã tồn tại, sẽ trả lại chỉ số tương ứng, nếu không, sẽ thêm vào và trả lại chỉ số mới nhất.

Quản lý các feature_node

/* Debug : Done
 * class-Feats.h
 *
 *  Created on: 2012/04/20
 *  Author: LUU TUAN ANH anh@jnlp.org
 */

#ifndef CLASS_FEATS_H_
#define CLASS_FEATS_H_
#include <set>
#include <vector>

using namespace std;

typedef pair<int, set<int>*> Feat;

class Feats {
    private:
        vector<Feat*> feats_;
    public:
        Feats() : feats_() {};

        ~Feats() {
            for (vector<Feat*>::iterator it = feats_.begin(); it != feats_.end(); ++it) {
                delete (*it)->second;
                delete *it;
            }
        }

        int size() const { return static_cast<int>(feats_.size()); }
        const vector<Feat*>* get() const { return &feats_; }
        void push_back(Feat* f) { feats_.push_back(f); };
};

#endif /* CLASS_FEATS_H_ */

Quản lý problem

/* Debug : done
 * class-Problem.h
 *
 *  Created on: 2012/04/20
 *  Author: LUU TUAN ANH  anh@jnlp.org
 */

#ifndef CLASS_PROBLEM_H_
#define CLASS_PROBLEM_H_
#include "linear.h"
#include "class-Feats.h"
//#include <fstream>
using namespace std;

template <class Feats>
class Problem {
    private:
        problem problem_;
    public:
        Problem(const Feats* feats, int strnum) {
                        // ofstream ofs(file_name);
            feature_node** x = new feature_node*[feats->size()];
            double* y = new double[feats->size()];

            for (int i = 0; i < static_cast<int>(feats->size()); ++i) {
                y[i] = double(feats->get()->at(i)->first);
                                 // ofs << y[i];
                feature_node* xx = new feature_node[feats->get()->at(i)->second->size()+1];
                x[i] = xx;
                int j = 0;
                for (set<int>::iterator it = feats->get()->at(i)->second->begin(); it != feats->get()->at(i)->second->end(); ++it) {
                    xx[j].index = *it;               
                    xx[j].value = 1;
                                        // ofs << " " << xx[j].index << ":" << xx[j].value;
                    ++j;
                }
                xx[j].index = -1;
                                // ofs << endl;
            }

            problem_.l = feats->size();
            problem_.n = strnum;
            problem_.y = y;
            problem_.x = x;
            problem_.bias = -1;
                        // ofs.close();
        }

        ~Problem() {
            for (int i = 0; i < problem_.l; ++i){
                delete[] problem_.x[i];
            }
            delete[] problem_.x;
            delete[] problem_.y;
        }

        problem* getProblem() { return &problem_; }
};

#endif /* CLASS_PROBLEM_H_ */

Để tạo ra các file có định dạng chuẩn Liblinear và Libsvm, bạn chỉ cần bỏ đi các dấu comment.


Quản lý parameter

/* Debug : done
 * class-Parameter.h
 *
 *  Created on: 2012/04/20
 *  Author: LUU TUAN ANH  anh@jnlp.org
 */

#ifndef CLASS_PARAMETER_H_
#define CLASS_PARAMETER_H_

#include "linear.h"

class Parameter {
    private:
        parameter parameter_;
    public:
        Parameter() {
            parameter_.solver_type = L2R_LR; // tùy theo dạng máy phân loại bạn muốn thực hiện
            parameter_.eps = 0.01; // mặc định
            parameter_.C = 1; // mặc định
            parameter_.nr_weight = 0;
            parameter_.weight_label = new int(1);
            parameter_.weight = new double(1.0);
        }

        Parameter(double c) {
            parameter_.solver_type = L2R_LR;
            parameter_.eps = 0.01;
            parameter_.C = c;
            parameter_.nr_weight = 0;
            parameter_.weight_label = new int(1);
            parameter_.weight = new double(1.0);
        }

        ~Parameter() {
            delete parameter_.weight_label;
            delete parameter_.weight;
        }

        const parameter* getParameter() const {return &parameter_; }
        void set_c(double c) { parameter_.C = c; }
};

#endif /* CLASS_PARAMETER_H_ */



Comments