KaggleのJigsawで銀メダルを獲得した解法等

# はじめに
就労移行支援チームシャイニーに所属するTK(TeamShiny in Akihabara)です。
つい先日、KaggleのJigsawコンペで113位となり銀メダルを獲得しました。
今回はそこで効果のあった自然言語処理の前処理の手法について軽く触れたいと思います。
コンペについての詳細はコード、スライドをあげているのでそちらも併せてご覧ください。
また、Qiita初投稿のため不躾なところがありましたらすみません。

<!-- 各チャプター -->
<a id="#reference"></a>
# 資料等
- [コード](https://www.kaggle.com/toshihikok/113th-solution)
- [スライド](https://www.docswell.com/s/2600123617/58M475-2022-03-07-204955)

<a id="#Chapter1"></a>
# コンペ概要
Jigsawコンペは自然言語のデータから文章の有毒性を判定する問題です。

Public Leaderboardスコア上位の公開コードはPublic Leaderboardに過剰に適合しており、
Private Leaderboardでは順位が大きく変動するなど、
全体的には荒れていたコンペと言えると思います。

<a id="#Chapter2"></a>
# EDA

文章を眺めてはみましたが、英語の文章のため正直なところなかなかピンときませんでした。
Kaggle上で高い評価を受けている[この公開コード](https://www.kaggle.com/kishalmandal/most-detailed-eda-tf-idf-and-logistic-reg)や[この公開コード](https://www.kaggle.com/robikscube/jiggsaw-toxic-comments-eda-twitch-stream)のEDAが
ある程度ポイントを押さえており有用そうな印象で一読はしました。
放送禁止用語的な言葉が有害な傾向のようです。

<a id="#Chapter3"></a>
# 特徴量の結合

普通にTransformerを用いたディープラーニングによる解法も試しましたがそちらは
パッとしなかったので、自然言語を特徴量として抽出する前処理に着目し
試行錯誤をしました。
Hidehisa Araiさんの[テーブルデータ向けの自然言語特徴抽出術](https://zenn.dev/koukyo1994/articles/9b1da2482d8ba1)を参考に何種類か手法を試して
CVの良かった手法を採用しました。

その一例
BertSequenceVectorizer (unbiased-toxic-roberta)

```
import numpy as np
import torch
import transformers

from transformers import RobertaTokenizer


class BertSequenceVectorizer:
    def __init__(self, model_name="bert-base-uncased", max_len=128):
        self.device = "cuda" if torch.cuda.is_available() else "cpu"
        self.model_name = model_name
        self.tokenizer = RobertaTokenizer.from_pretrained(self.model_name)
        self.bert_model = transformers.RobertaModel.from_pretrained(self.model_name)
        self.bert_model = self.bert_model.to(self.device)
        self.max_len = max_len

    def vectorize(self, sentence: str) -> np.array:
        inp = self.tokenizer.encode(sentence)
        len_inp = len(inp)

        if len_inp >= self.max_len:
            inputs = inp[:self.max_len]
            masks = [1] * self.max_len
        else:
            inputs = inp + [0] * (self.max_len - len_inp)
            masks = [1] * len_inp + [0] * (self.max_len - len_inp)

        inputs_tensor = torch.tensor([inputs], dtype=torch.long).to(self.device)
        masks_tensor = torch.tensor([masks], dtype=torch.long).to(self.device)

        bert_out = self.bert_model(inputs_tensor, masks_tensor)
        seq_out = bert_out['last_hidden_state']
#         seq_out = self.bert_model(inputs_tensor, masks_tensor)[0]
#         pooled_out = self.bert_model(inputs_tensor, masks_tensor)[1]

        if torch.cuda.is_available():    
            return seq_out[0][0].cpu().detach().numpy() # 0番目は [CLS] token, 768 dim の文章特徴量
        else:
            return seq_out[0][0].detach().numpy()
```

上で定義したBertSequenceVectorizerとTfidfVectorizerで抽出した特徴量を
scipy.sparse.hstackで連結しています。

```
import pandas as pd
import numpy as np
from sklearn.linear_model import Ridge
from sklearn.svm import LinearSVR
from sklearn.feature_extraction.text import TfidfVectorizer,HashingVectorizer,CountVectorizer
import scipy
from scipy.stats import rankdata
import nltk

stop_words = nltk.corpus.stopwords.words('english')

jr = pd.read_csv("../input/jigsaw-regression-based-data/train_data_version2.csv")
jr.shape
df = jr[['text', 'y']]
df = df.drop_duplicates()
BSV = BertSequenceVectorizer(model_name="../input/unbiasedtoxicroberta/unbiased-toxic-roberta", max_len=512)
X1 = np.stack(df["text"].map(lambda x: BSV.vectorize(x).reshape(-1)).values)
vec2 = TfidfVectorizer(analyzer='char_wb', max_df=0.7, min_df=1, ngram_range=(2, 5), stop_words=stop_words)
X2 = vec2.fit_transform(df['text'])
X = scipy.sparse.hstack([X1, X2])
```

# まとめ
上記の手法などを駆使し、コンペはまずまずの結果が残せました。
LBが高いsubmitも捨てきれず半信半疑でしたが最終的には「Trust CV」が正解でした。
また、きちんと分析ができなかったことも反省点の一つでした。今後の教訓となりました。

複数の前処理を結合して特徴量を増やすことにより、
スパースなどのデータで有効なリッジ回帰などの線形モデルで一定の精度が出せました。