ngram+ロジスティック回帰でDiscordのメッセージを監視したお話

# はじめに
Discordサーバーを運営していると時々、ルール通りに文言を投稿しない人が出てきますよね。
その都度指摘してはまた別の人が繰り返しまた、指摘は繰り返し...（以下略）
※今回の記事は自分の知識整理・備忘録として記載しています。

### ある日
![unnamed.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4111030/355de564-8425-4434-9194-42642f006553.png)

### 何をしたの？
チャンネルで下記のような文言にそっているかDiscordボットで監視をして、その通りでなければ書き直すように指摘するボットを作りました。

作品名：
キャラの正確な名前：
英語の綴り：
希望する内容：
その他要望：

#### フローチャート
```mermaid
flowchart TB
    A[メンバーがメッセージ投稿] --> B{依頼文か?} -- 依頼文ではない --> C[無視する]
    B{依頼文か?} -- 依頼文である --> D{形式に添っているか？} -- 添っている --> E[無視する]
    D{形式に添っているか？} -- 添っていない --> F[指摘する]

```

### 何の技術を使ったの？
ngram・LogisticRegression・TfidfVectorizer

### 使った言語は？
Python

# 実装

### 学習データ
下記のような形のデータを用意する。依頼だと思われるmsgに対しては1とラベル付けした。
※出所は自分のDiscordサーバーに投稿されている1万3500件程度のデータを人力でラベル付けした。

|msg |req |
|--- |--- |
|よろしくお願いします。|0|
|AIについての学習のため参加しました。|0|
|忙しい所悪い！この辺って依頼してもいいっすか？？|1|
|走っている少年の絵を描いてもらってもいいですか？|1|
|...|...|


### データ読み込み
```python
texts = get_csv_column_as_list('dataset.csv', 'msg')
is_requests = get_csv_column_as_list('dataset.csv', 'req')
is_requests = list(map(int, is_requests))
```

### データフレーム作成
```python
from janome.tokenizer import Tokenizer

# Janome初期化
t = Tokenizer()

def tokenize_text(text):
    tokens = [
        token.surface
        for token in t.tokenize(text)
        if token.part_of_speech.split(',')[0] in ['名詞', '動詞', '形容詞']
    ]
    return " ".join(tokens)
    
df = pd.DataFrame({
    'text': texts,
    'is_request': is_requests
})
df['tokenized_text'] = df['text'].apply(tokenize_text)
```
全メッセージを形態素解析で品詞分解する。細かく分けることで、日本語の依頼っぽい文章の検出をやりやすくする。

### パイプラインでまとめる
```python
# パイプライン定義
pipeline = Pipeline([
    ('tfidf', TfidfVectorizer(ngram_range=(1, 2), max_features=1000)),
    ('clf', LogisticRegression(random_state=42, solver='liblinear', class_weight='balanced'))
])
```
#### tfidfの箇所
一段目のtfidfは文章(文字列)を数値のベクトルに変換することを目的としている。
・```TfidfVectorizer```:単語の出現頻度を元にTF-IDFベクトルを作るクラス。
・```ngram_range```:1-gram(単語)と2-gram(隣接する2単語の組)の両方を特徴量として使う。

:::note info
例えば
「猫が好き」⇒「猫」「が」「好き」と「猫が」「が好き」に分けられる。
:::
・```max_features```:TF-IDF特徴量のうち上位何個までを使うか
#### clfの箇所
二段目のclfはtfidfでベクトル化されたテキストを使って、分類を行うことを目的としている。
```LogisticRegression ```:2クラス(または多クラス)の分類に使えるモデル。
```random_state```:乱数シード値。本記事のプログラムは再現性を保つために固定する。
```solver='liblinear'```:小～中規模のデータに適した計算アルゴリズム
```class_weight='balanced'```:クラスに偏りがある場合、自動的にバラスを取ることで少数クラスを無視されにくくする。

### クロスバリデーションによる分割
```python
# スコア指標
scoring = {
    'precision_macro': 'precision_macro',
    'recall_macro': 'recall_macro',
    'f1_macro': 'f1_macro',
    'accuracy': 'accuracy'
}

# クロスバリデーション（5分割）
cv_results = cross_validate(
    pipeline,
    df['tokenized_text'],
    df['is_request'],
    cv=5,
    scoring=scoring,
    return_train_score=False
)
```
プログラムを言葉で説明すると、事前に定義したpipelineを使って、データである`df['tokenized_text']`(入力)と`df['is_request']`(正解ラベル)を用いて、5分割で交差検証(cv=5)を行い、精度を計測する。

・`scoring`:学習モデルの評価指標を複数指定できる。今回の場合、適合率・再現率・F1スコア・精度が出力される。

・`return_train_score=False`:テスト側だけを評価する。Trueにすると、学習データ・テストデータの両方を評価する。

:::note info
5分割で交差検証とは
データを5つに分割して、それを5回繰り返して学習と評価を行う。
※各回で、1つをテスト、残り4つを訓練に使う。
:::

##### 5分割交差検証結果
|precision_macro|recall_macro|f1_macro|accuracy|
|---------------|------------|--------|--------|
|0.615|0.838|0.654|0.913|

再現率が0.838と依頼の見逃しは少ないが、適合率が0.6程度なので、依頼と予測したが実際は違った(誤検出)もまぁまぁあるという結果に。まだまだ改善の余地はありそう。

### 動作確認
依頼系のメッセージ（ボットが反応するのが正解）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4111030/327da2b6-5426-472e-b7df-3baf79e522c7.png)

依頼系のメッセージだが、形式通りのもの（ボットが反応しないのが正解）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4111030/8a827a7e-61e2-4a3f-838a-763de8a5cfcd.png)


依頼系メッセージではないもの（ボットが反応しないのが正解）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4111030/1ccd7c47-a14b-429e-9891-474402bbc3e8.png)





次回もあるかも？

# 参考文献
・ChatGPT

https://gotutiyan.hatenablog.com/entry/2020/09/10/181919

https://free.kikagaku.ai/tutorial/basic_of_machine_learning/learn/machine_learning_hyperparameters