NLP問題にもマスクを着けてみた

# 目次
[はじめに](#はじめに)
[1. Mask Augmentation](#1-mask-augmentation)
[2. 疑似ラベリング](#2-疑似ラベリング)
[終わりに](#終わりに)
[参考文献](#参考文献)

# はじめに
　この記事は[BrainPad Advent Calender 2022](https://qiita.com/advent-calendar/2022/brainpad) 23日目の記事です。
　街中でも某巨大SNS関連のニュースでも"マスク"をよく目にしますね。では、自然言語処理(NLP)関連にはどんな"マスク"があるのでしょうか？今回はMask Augmentationと疑似ラベリングという2つの"マスク"について書いてみたいと思います。

　ちなみにこちらはStable Diffusionで"NLP problem wearing a mask"と打って生成された画像です。脳内で言語処理している感じ+マスク+アートといった感じでしょうか。
<img width="300" alt="NLP_mask_1.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2819962/7be816a5-3367-a53d-0af7-dbd5157b76c9.jpeg">
# 1. Mask Augmentation
## 1-1.手法概要 
　NLPでのAugmentation技術の一つで、下記のように文書の一部を[MASK]に置き換えたものを元のデータに追加するというものです。

|Mask前|Mask後|
|:--:|:--:|
|“This is a test. Ok, now what?”|“[MASK] is a test. Ok [MASK] [MASK] what ?”|

## 1-2.論文紹介
　<a href = "https://www.jstage.jst.go.jp/article/jnlp/28/2/28_721/_pdf">An Empirical Study of Contextual Data Augmentation for Japanese Zero Anaphora Resolution</a>において、Mask Augmenationを用いた精度検証が行われています。こちらでは、マスクする品詞を様々に変えています。
![MaskAugmentation_論文図引用.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2819962/e3260058-45fd-e219-1b32-19f506a7f3be.png)

　結論からお伝えすると、以下のようにマスクをかけた時の精度が最も良かったとの事です。

①動詞以外をマスク(all\verb)
②50%をマスク

## 1-3.実装
　それでは、ここで実装してみたいと思います。
　以下、実行環境です。
```
!uname -svr
Linux 5.10.133+ #1 SMP Fri Aug 26 08:44:51 UTC 2022
!python --version
Python 3.8.16
!nltk --version
nltk, version 3.7
```
　以下、実装コードです。
```python:Maskaugmentation実装
import random
import nltk 
nltk.download('punkt')
nltk.download('averaged_perceptron_tagger') 

mask = "[MASK]"
#posタグリスト(動詞のみ)
#参考 : https://www.learntek.org/blog/categorizing-pos-tagging-nltk-python/
pos_lst = ["VB", "VBD", "VBG", "VBN", "VBP", "VBZ"]

def mask_aug(text, ratio, seed):
    #①word_tokenizeでtokenize
    tokens = nltk.word_tokenize(text)
    random.seed(seed)
    for _ in range(int(len(tokens)*ratio)):
        #②tokenizeしたものにposタグをつける
        pos = nltk.pos_tag(tokens)
        #③2iter以降は[MASK]タグを除外
        if _ != 0:
          pos = [i for i in pos if i[0] != "[MASK]"]
        #④VB関連以外を取り出し、単語をランダムに選ぶ
        tokens_pos = [i[0] for i in pos if i[1] not in pos_lst]
        word = random.choice(tokens_pos)
        #⑤ランダムに取り出した単語がtokenzの中の何番目に位置するか(複数ある場合は全て取り出す)
        idx_lst = [idx for idx, _ in enumerate(tokens) if tokens[idx] == word] 
        #⑥そこからランダムに一つ選び、[MASK]化
        idx = random.choice(idx_lst)
        tokens[idx]  = mask

    return " ".join(tokens).replace(" .", ".").replace(" ,", ",")

#テスト実行
text = "This is a test. Ok, now what?"
mask_aug(text, 0.3, 0)
```
```:結果
[MASK] is a test. Ok, [MASK] what [MASK]
```
　こちらでは、**1-2**のご説明の通り動詞以外をマスクしています。その際、[Categorizing and POS Tagging with NLTK Python](https://www.learntek.org/blog/categorizing-pos-tagging-nltk-python/)を参考にPOSタグを取り出して来ています。また、ここでは30%でマスクをしましたが、この数値も引数の"ratio"で調整できます。

## 1-4.実際に使ってみた
　こちらを踏まえて、2022年8-11月に開催されていたKaggleの[Feedback Prize - English Language Learning](https://www.kaggle.com/competitions/feedback-prize-english-language-learning)コンペで実装・精度検証してみました。CVを比べてみると、確かに①の動詞以外をマスクする手法は効いたのですが、②のマスクする割合は15%くらいの方が精度が良かったです。

# 2. 疑似ラベリング
## 2-1. 疑似ラベリングとは何か？
　疑似ラベリング(Pseudo Labeling)とは、Deep Neural Networkを半教師あり学習形式で構築する手法の事を言います。

　例えば100枚のラベル付き画像と1000枚のラベル無し画像があったとして、以下のような手順を取ってモデルを構築します。
1. 100枚のラベル付き画像で学習しmoodel1を作成
2. 1000枚のラベル無し画像を追加し、計1100枚で改めて学習してmodel2を作成

　これにより、model1よりもmodel2の方が汎化性能が上がるとされています。

　NLPデータに仮面舞踏会で着るようなかりそめ(≒疑似)の"マスク"を着けるようなイメージです!(強引)
## 2-2. 実際の活用例と活用方法
　[上記のKaggleコンペ](https://www.kaggle.com/competitions/feedback-prize-english-language-learning)で実際に疑似ラベリングが使用されていたので、紹介します。
### 2-2-1.コンペ概要
* 8th-12th grade(中2～高3くらい)かつ英語が第2外国語の生徒が書いたエッセイがtrain data(下記参考図)。
![コンペ概要.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2819962/37670f86-5e12-11a4-f5b1-1674e14d76f5.png)
* 6つの尺度(cohesion, syntax, vocabulary, phraseology, grammar, conventions)があり、そのスコアをそれぞれ予測するというタスク
* 評価指標はMCRMSE(mean columnwise root mean squared error) - 各列(尺度)毎にRMSEを計算し、その平均を取る
### 2-2-2.疑似ラベリングの使いどころ
　こちらのコンペでは3回目のFeedback Prizeコンペだったため、過去のデータを疑似ラベリングとしてTrain Dataにいかに組み込むか、というのが大きなトピックの一つでした。
### 2-2-3.疑似ラベリング使用上の注意
　[after training with pseudo labels CV score increased but, ...](https://www.kaggle.com/competitions/feedback-prize-english-language-learning/discussion/352001#1936277)というDiscussionにてChris Deotteさんが書かれている内容が非常に参考になりました。具体的には、以下の点を注意する必要があるそうです。
* model_foldx(xはfold number)で予測した疑似ラベル付きのデータは、model_foldxを作成した時と同じfold分けで再学習する必要がある(でないとリークになる)。
* 疑似ラベルデータが多すぎたりあまり信用できない場合、各fold毎の疑似ラベルの標準偏差を取り、ばらつきが少ないデータを再学習に使う。

## 2-3. なぜ疑似ラベリングが効くのか？
　それでは、なぜ疑似ラベリングによりモデルの性能が上がるのでしょうか？ここでは、[Pseudo-Label : The Simple and Efficient Semi-Supervised Learning Method for Deep Neural Networks](https://www.researchgate.net/publication/280581078_Pseudo-Label_The_Simple_and_Efficient_Semi-Supervised_Learning_Method_for_Deep_Neural_Networks)を参照しつつ書きたいと思います。この論文は2013年に発表され、これまでで2400以上の論文で引用されています。
### 2-3-1. ざっくりとした理由説明
　本論文では分類問題について議論しています。ざっくりと疑似ラベリングが効く理由を説明すると、以下になります。
①疑似ラベリングによってclass overlapが減る
②class overlapが少ない程、汎化性能が上がる
③よって、疑似ラベリングによって汎化性能が上がる

　この"class overlap"を、以下のテーブルでご説明します。

|            | **x_1** | **x_2** |**x_3** |
|------------|---------|---------|---------|
| **model1** | 0.1     | 0.9     | 0.9     |   
| **model2** | 0.4     | 0.6     |0.5     |   

　ここでは、2値分類問題を想定しています。入力x_1、x_2、x_3に対して、model1、model2はそれぞれテーブルのような予測確率を出力したとします。model1ははっきりと分類出来ており、model2は確率が比較的0.5近辺に密集しています。このmodel1の出力状態においてはclass ovelapが少なく、model2はその逆です。つまり、class overlapが少ないとクラスがはっきりと分類可能という事です。

### 2-3-2. 数式を用いた説明
　ここでは上記2-3-1.「①疑似ラベリングによってclass overlapが減る」の理由を数式と図で説明します。

#### 1.全体の損失関数
　まず、疑似ラベリングを用いた場合の全体の損失関数は以下になります。

```math
L = \frac{1}{n}\sum_{m=1}^{n}\sum_{i=1}^{C}L(y_i^m, f_i^m) + \alpha(t)\frac{1}{n'}\sum_{m=1}^{n'}\sum_{i=1}^{C}L(y'_i{^m}, f'_i{^m})\qquad(1)
```

　以下、各記号の意味です。

|$$L$$|$$n$$|$$m$$|$$C$$|$$y$$|
|-----|-----|-----|-----|-----|
|損失関数|バッチサイズ|バッチ内の各入力データidx|クラス数|正解ラベル|

|$$f$$|$$\alpha$$|$$t$$|$$'$$|
|-----|-----|-----|-----|
|予測関数|係数|エポック数|疑似ラベリング|

　数式(1)の右辺第1項は元のデータに対しての損失、第2項は疑似ラベリング後のデータに対しての損失を表しています。

　ちなみにこのαは疑似ラベリング後のデータのみの損失を全体の損失にどれくらい反映させるかを調整する係数となっており、以下のような計算になっています。


```math

\alpha(t) = \left\{
\begin{array}{lll}
0 & t < T_1 \\
\frac{t - T_1}{T_2 - T_1} \alpha_f & T_1 \leq t < T_2 \\
\alpha_f & T_2 \leq t
\end{array}
\right.

```
　仮に$\alpha_f = 3, T_1 = 100, T_2 = 600$だったとすると、以下のような場合分けになります。

```math

\alpha(t) = \left\{
\begin{array}{lll}
0 & t < 100 \\
0.006t - 0.6 & 100 \leq t < 500 \\
3 & 500 \leq t
\end{array}
\right.

```

　このように学習を進めるにつれて徐々に疑似ラベリングの損失を全体に反映させていきます。これにより、より疑似ラベルが本来のラベルに近づくそうです。
#### 2.Conditional Entropy
　次に、以下のようなConditional Entropyという関数を考えます。
```math
H(y|x') =- \frac{1}{n'}\sum_{m=1}^{n'}\sum_{i=1}^CP(y_i^m = 1|x'^m)\log P(y_i^m = 1|x'^m) \qquad(2)
```
　この関数により、疑似ラベリング後の入力を用いた各予測ラベルが1である確率($P(y_i^m = 1|x'^m)$)が、1か0に寄っているか(≒class overlapが減っているか)、が分かります。

　もう少し、図を交えて説明します。
　(2)右辺の$P(y_i^m = 1|x'^m)\log P(y_i^m = 1|x'^m)$の部分だけ取り出し、さらに$P(y_i^m = 1|x'^m)$を$p$と置き換え、下のような関数を考えます。
```math
f(p) = p\log p
```
　これを描画すると以下のようになります。
<img width=400 alt="conditional_entropy.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2819962/30917c0a-09e0-9aa7-cb8b-3e3decddd89e.png">
　$p$が0か1に近いほど、$f(p)$は0に近づく事が分かります。
　つまり、(2)の数式全体と合わせて考えると、**$H(y|x')$はclass overlapが少ないほど(クラスがよりはっきり分類可能になっていればいるほど)値がより0に近づく**、と言えます。
#### 3.全体の損失関数の式変形
　ここで、(1)の全体損失関数を以下のように書き直します。

```math
C(θ, λ) = \sum_{m=1}^{n}\log P(y^m|x^m; θ) - λH(y|x';θ) \qquad(3)
```
　以下、各記号の意味です。
|$$θ$$|$$λ$$|$$n$$|$$x^m$$|
|-----|-----|-----|-----|
|パラメータ|係数|初めからラベル付けされているデータ数|m個めのサンプル|

　右辺第1項は(1)の右辺第1項と、第2項は(1)の右辺第2項と連動しています。ちなみ第2項の$λ$は(1)の$α$と連動していて、また$H(y|x';θ)$は(2)と同じです。

　この(3)を最大化するように学習を進めます。つまり、これまでの話を踏まえると、**class overlapを下げて汎化性能を上げるように学習がすすむ**という事になります。

　ちなみに(3)は[最大事後確率(maximum a posteriori, MAP)推定](https://ja.wikipedia.org/wiki/%E6%9C%80%E5%A4%A7%E4%BA%8B%E5%BE%8C%E7%A2%BA%E7%8E%87)という、確率が最大の予測値を得る事が出来る推定手法を用いています。


　以上が、なぜ疑似ラベリングが効くのか？の説明になります。
　
　今回は分類問題に限っていますが、回帰問題だった2-2.のKaggleコンペでも疑似ラベリングが効いたので、どちらでも使用可能のはずです。
## 2-3.補足
　2-1.の冒頭や2-2-3.でご紹介したKaggleコンペでの疑似ラベリング方法は、論文内で説明されている方法と若干異なります。具体的には、(1)の$α$のような係数を用いて疑似ラベリングの損失関数を徐々に全体関数に加えていくという事はせず、単純に学習→疑似ラベリング→疑似ラベルも含めて再学習、という流れになっています。

# 終わりに
　今回はNLP関連の2つの"マスク"をご紹介しました。どちらも実践的で使える手法だと思うので、ぜひ参考になれば幸いです。
　それでは、メリークリスマスク！
# 参考文献
* [An Empirical Study of Contextual Data Augmentation for Japanese Zero Anaphora Resolution](https://www.jstage.jst.go.jp/article/jnlp/28/2/28_721/_pdf)
* [Pseudo-Label : The Simple and Efficient Semi-Supervised Learning Method for Deep Neural Networks](https://www.researchgate.net/publication/280581078_Pseudo-Label_The_Simple_and_Efficient_Semi-Supervised_Learning_Method_for_Deep_Neural_Networks)
* [最大事後確率(maximum a posteriori, MAP)推定](https://ja.wikipedia.org/wiki/%E6%9C%80%E5%A4%A7%E4%BA%8B%E5%BE%8C%E7%A2%BA%E7%8E%87)