LightGBMのweightの使い方、weightは一体何を行っているのか

# 簡潔に

- LightGBMのパラメータであるweightの実装方法と、何を行っているのかを説明した。
- ざっくり言うと、各行に重みを与え、重みが大きい行が学習時に重視されるようになる。
- 詳しく言うと、weightは学習時のgradiant,hessian,lossの変化に影響を与え、weightが大きい行が木の分岐点決定において重視されるようになる。
- H2O.aiのサイトでは、`weightが2だった場合は、該当の行を複製(2行に増やす)ことと同じです。`と説明されている。

# 初めに

Kaggleで大人気のLightGBMにはweightというパラメータが存在します。上位ソリューションでは、このパラメータを使った解法が比較的多いと感じていますが、weightを日本語で解説している記事があまり多くなかったため、説明記事を自分で作成しました。

# weightの使い方

使い方を先に記載します。コードの下記部分に追加を行います。
コードは[公式Document](https://lightgbm.readthedocs.io/en/latest/Python-Intro.html#data-interface)より引用してます。

```python:公式の例
w = np.random.rand(500, )
train_data = lgb.Dataset(data, label=label, weight=w)#ここ
```

上記の例ではnumpyの1-D arrayでweightを掛けていますが、
pandasのseriesかlistでも問題ありません。その説明は、github上の[ここ](https://github.com/microsoft/LightGBM/blob/master/python-package/lightgbm/basic.py#L1486)に記載があります。

```:公式の説明
 weight : list, numpy 1-D array, pandas Series or None
            Weight to be set for each data point.
```

個人的には、dataframeに`weight`というcolumnを追加し、
下記のようにした方が分かりやすいと思います。

```python:自分の使い方
lgb_train = lgb.Dataset(
    X_train.drop("weight", axis=1), #学習には用いないためweightを落とす
    y_train, 
    weight=X_train["weight"] #ここ
)

lgb_eval = lgb.Dataset(
    X_valid.drop("weight", axis=1), #同様にweightを落とす
    y_valid, 
    reference=lgb_train, 
    weight=X_valid["weight"] #ここ
)
```

たまにlgb_evalにはweightを記載しない例を見ますが、[下記公式の例](https://github.com/microsoft/LightGBM/blob/69c1c33093cb9fa1afd896b8a458e47aa2930c92/examples/python-guide/advanced_example.py#L29)にてvalidにも記載しているため、lgb_evalにもweightは必要かと思います。

```python:公式の例
lgb_train = lgb.Dataset(X_train, y_train,
                        weight=W_train, free_raw_data=False)
lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train,
                       weight=W_test, free_raw_data=False)
```


# weight とは
まずは、公式の説明を探してみましょう。
[ここ](https://lightgbm.readthedocs.io/en/latest/Parameters.html#weight-data)でweightの説明があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/390595/f544114e-a468-a079-0676-95e0edd69a96.png)

まとめると

- LightGBMは重み付け学習が可能だよ
- 追加ファイルを使う場合は、重みファイルをdata fileと同じフォルダに置いてね。（今回こちらの使い方は触れません。）
- パラメータ`weight_column`で指定することもできるよ。(この使い方も触れません。)

という感じでした。

weightの使い方は説明してくれていますが、そもそもweightが何をしているのかは触れてないですね・・・🤔(公式documentのどこかで説明あるんですかね？ご存知の方いたら教えてください。）

というわけで、ここから先は自身で調べたweightの意味合いを記載していきます。

# weightが行っていること
### ざっくりした理解

ざっくりな説明をすると、weightが大きい行をより重視し、weightが小さい行はあまり重視しなくなります。

もし下記のようなweightの掛け方をした場合、２行目(性別が女性)がより重視されます。何かしらの理由で女性のデータが重要な場合に使えますね。

|Sex|Height|weight|
|----|----|----|
|Male|180|0.3|
|Female|160|0.7|
|Male|175|0.3|

### しっかりした理解

「重視とは、具体的に何をしているのだろうか・・・🤔」となると思うので、ここからはgithub上でのLigthGBMのコードや数式を用いて説明を行います。(kaggler-jaの[私の質問](https://kaggler-ja.slack.com/archives/C6RV8M3DG/p1587475873046300)に対する回答をベースにしています。ご回答ありがとうございます。)

まずweightを使っているコードは[下記部分](https://github.com/microsoft/LightGBM/blob/master/python-package/lightgbm/basic.py#L1486)となります。

```python:basic.py_L1486~
def set_weight(self, weight):
        """Set weight of each instance.
        Parameters
        ----------
        weight : list, numpy 1-D array, pandas Series or None
            Weight to be set for each data point.
        Returns
        -------
        self : Dataset
            Dataset with set weight.
        """
        if weight is not None and np.all(weight == 1):
            weight = None
        self.weight = weight
        if self.handle is not None and weight is not None:
            weight = list_to_1d_numpy(weight, name='weight')
            self.set_field('weight', weight)
            self.weight = self.get_field('weight')  # original values can be modified at cpp side
        return self
```

`self.weight = self.get_field('weight')  # original values can be modified at cpp side`
とあるため、weightの計算はcppファイルにて行われているようです。

このcppファイルですが[objective_function.cpp](https://github.com/microsoft/LightGBM/blob/master/src/objective/objective_function.cpp)が(多分)該当し、このファイルは同じフォルダにおいてあるhppファイルを参照しにいきます。そのため、次はweightを使用しているhppファイルの一つである[regression_objective.hpp](https://github.com/microsoft/LightGBM/blob/master/src/objective/regression_objective.hpp#L323)を見にいきましょう。

本ファイル内には、下記のような行が存在します。

```C++:regression_objective.hpp_L372~
else {
      #pragma omp parallel for schedule(static)
      for (data_size_t i = 0; i < num_data_; ++i) {
        const double diff = score[i] - label_[i];
        if (std::abs(diff) <= alpha_) {
          gradients[i] = static_cast<score_t>(diff * weights_[i]);
        } else {
          gradients[i] = static_cast<score_t>(Common::Sign(diff) * weights_[i] * alpha_);
        }
        hessians[i] = static_cast<score_t>(weights_[i]);
      }
    }
```

`gradients[i] = static_cast<score_t>(diff * weights_[i]);`
この行ではgradientにweightをかけています。

`hessians[i] = static_cast<score_t>(weights_[i]);`
この行ではhessianにweightをかけています。

つまり、LightGBMの学習時に用いるgradientとhessianにweightが掛けられることが分かりました。

「gradientとhessianにweightがかかる・・・つまりどういうことだ・・・🤔」と私はなったゆえ、追加で説明していきます。
(ここからはあまり自信がありません。間違いありましたら指摘お願いします。)

まず、LightGBMは各特徴量の分岐点を決定する際、XGBoostと同様の方法でlossの計算を行っていると思われます。木を構築する際、分岐後のlossが最も小さくなるような分岐点を選びます。

JackさんによるXGBoostの[解説スライド](https://speakerdeck.com/rsakata/santander-product-recommendationfalseapurotitoxgboostfalsexiao-neta?slide=67)の式をお借りすると、下記のような式でlossの計算を行っています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/390595/5c1a921f-3262-42d5-8e24-1a53d585a9b1.png" width=50%>

$ΔL$は分岐前後のlossの変化であり,
$g_i$がgradient, $h_i$がhessianです。
また$Σ$はデータ各行の総和であることを意味します。
($λ$は正則化項のパラメータです。今回は関係ありません。)

つまり、gradientとhessianにweightをかけると、

- weightが大きいデータに対しては、gradientとhessianが同様に大きくなる。分子のgradientは二乗されているため、$ΔL$は負の方向に大きくなる。(=分岐後のlossがより小さくなる。)
- LightGBMは、分岐後のlossがより小さくなる方へデータの分岐点を決定する。
- よって、weightが大きいデータのlossが下がる方へ、分岐点が決定されやすくなる。

上記より、
weightが大きい行がlossの変化に影響を与え、木の分岐点決定においてより重視されるようになることが分かりました。

# おまけ　
機械学習プラットフォームである[H2O.aiのサイトの説明](http://h2o-release.s3.amazonaws.com/h2o/rel-wheeler/2/docs-website/h2o-docs/data-science/algo-params/weights_column.html)には`weights_column`の説明が下記のようになっています。

```
For example, a weight of 2 is identical to duplicating a row.
```
意味としては
`weightが2だった場合は、該当の行を複製(2行に増やす)ことと同じです。`
という感じですね。

確かにgradientとhessianの考え方を当てはめると、上記のように理解して問題なさそうです。直感的で分かりやすいですね。

# 最後に

疑問点、おかしい部分などあれば遠慮なく教えてください。