LO-BLEACHポエム分類器を作ってみた

世の中では、すでに様々なAIが活用されていますね。商品を推薦したり、不良品を検知したりと人の役に立つものがたくさんあります。

が、何もAIの全てがそんな人の役に立つためにあるとも限りません。世の中には、聞いた人が「ハァ、馬鹿じゃねえの？」と思わず言ってしまうような目的のために、半ばお遊びで作られるあほあほなAIも存在します。これはそんなあほあほAIのひとつです。人の役には立たないかもしれませんが、少なくとも機械学習の勉強をするきっかけくらいにはなるんじゃないですかね（投げやり）。あと、作ってる本人も__正気に戻るまでは__面白いです。この記事を書いている時点では__正気に戻っている__ので、こいつ何考えてたんだろう……と思いながらぽちぽち文章を入力しています。虚しいですね。時間の無駄です。でも記事を投稿して、タイトルだけでも見て「馬鹿じゃねえの？」と一瞬でも笑わせられたら勝ちです。

ここはひとつ、そんなあほあほAIの作成記録に付き合ってあげるぜ！という~~暇人~~おおらかな方々はどうぞ以下の記事をお楽しみください。

※注意点
この記事はとりあえず作ってみた、という趣旨の記事です。細かいソフトやライブラリの導入方法などはすっ飛ばしているので、その点ご注意ください。

# 概要

* LOとBLEACHのポエム分類器を作ったよ！
* 文書ベクトル化には日本語wikipedia word2vecモデル+SWEMを使ったよ！


# そもそものはじまり
在宅勤務なのをいいことに、昼休みにアニメを消化し、さらに余った時間でVtuberの切り抜き動画を巡回するのがここ最近の日課です。いやぁ、ツボを押さえた切り抜き動画は昼休みの短時間でゲラゲラ笑えて良いものですね。昼休みに一人で人目を気にせず馬鹿笑いすることで、午後の英気を養います。__これは必要なことです__。

さて、そんな切り抜き動画を巡回していた数日前のこと。投稿日は半年以上も前であったものの、たまたま目に入った動画がありました。

[2.0お披露目配信でイチローの名言をLOとBLEACHに分類する卯月コウ
](https://www.nicovideo.jp/watch/sm36026037)

LOとBLEACHのポエムを混ぜてクイズ大会をしよう。１ヶ月ほど前にはTwitterのトレンドにも関連ワードが入り、さらには[ねとらぼの記事](https://nlab.itmedia.co.jp/nl/articles/2006/05/news107.html)にもなっています。
元はといえば、この動画の切り抜き元でもある卯月コウさんが、さらにこの一年近く前の配信で発案されたことです。それがなんやかんやあり、ちょくちょく話題に上がることを繰り返していたそうです。その辺りの事情はあまり詳しくないので割愛します。

ともかく、上記の切り抜き動画をひとり笑いながら見ていたところ、とあるひとつのコメント[^1]が目に止まりました。それは、なぜかイチローの名言をLOとBLEACHに分類し始めた、卯月コウに対する反応です。

「機械学習による2値判別かな?」

__その時、電流が走った。__[^2]

明らかにブームは過ぎている。しかし、やらねばならぬ。私がやらねば誰がやる。この世全てのありとあらゆるポエムを、LOとBLEACHに判別できるAIを作らねばならぬ。
だって、面白そうだから。

# LO-BLEACHポエム分類器の作成

## 目的

* 与えられたポエムを、LOかBLEACHに__わざわざAIに分類させて__楽しむ

本プロジェクトの目的は以上です。間違っても、100%の精度で完璧にLOとBLEACHを分類できるすーぱーAIを作ることが目的ではありません。単にAIもとい機械学習で遊ぶことが目的です。あほあほAIはそれ自体を作って遊ぶことを目的にしてもOKです。

……というか、そもそも使えるデータの問題により精度を追求するのはほぼ不可能でしょう。

## 対象データ

* [LOの表紙ポエムVol.1〜197](https://dic.nicovideo.jp/a/comic%20lo%E3%81%AE%E8%A1%A8%E7%B4%99%E3%82%AD%E3%83%A3%E3%83%83%E3%83%81%E3%82%B3%E3%83%94%E3%83%BC%E3%81%AE%E4%B8%80%E8%A6%A7)　※ただし、ポエムのないVol.14は除く
* [BLEACH全74巻の巻頭ポエム](https://marutopi.com/book/comic/bleach/poem/)

さて、この時点で機械学習にお詳しい方、でなくとも多少機械学習をかじったことがある方なら気づくことでしょう。

* LO：196件
* BLEACH：74件
* 合計データ数：270件

はい、どう考えても絶望的にデータが少ないです。
機械学習がやりたい！ってこのデータ持ってこられたら、そっと顔背けたくなるレベルで少ないです。でも、片や月刊紙、片やコミックスの単行本です。そう考えるとむしろ充実しているのでは……？と思わなくもないですね。機械学習的に辛いことは変わりありませんが。これが画像データなら反転させたり回転させたりしてデータを増やせるのですが、ポエムは文書なのでそうもいきません。~~ポエムがないなら自分で書けばいいじゃない。~~
これが仕事なら絶望感に打ちひしがれるところですが、幸いにもあほあほAIをとにかく作成して遊ぶことが目的なので、ないものはないと諦めて突き進みます。

### 学習データ・テストデータ

機械学習をする以上、評価を行う必要があります。評価をしないなんてとんでもない。いくら精度を求めないとはいえ、何らかの判断基準は必要です。馬鹿なことを真面目にやるから面白いんだ。

というわけで、上記の全データからテストデータを選ぶ必要があります。ただでさえ少ない学習データがさらに減りますね。本来であればランダムにいくつか見繕うべきですが、今回は幸いにもお誂え向きなテストデータが既にネット上に存在していました。

[BLEACH、コミックLOキャッチコピークイズ](https://docs.google.com/forms/d/e/1FAIpQLSf0gx7_HzL4BS8ZZhhUjUhel2Thwl5hKq_1f-cst4klgyZ7lw/viewform)

そう、先日のTwitter上でバズるきっかけとなったクイズです。せっかくなので、今回はこちらのクイズの問題として出題されているポエム30件をテストデータとします。その都合上、後にクイズに含まれるポエムのLOとBLEACHの割合はネタバレしてしまうのでご了承ください。

学習データには、上記のテストデータ以外のポエム全てを用います。

## 分類器の作成
さて、ここからいよいよ分類器を作成していきます。

### どうやってポエムをベクトル化するか？

最初に立ちはだかる壁がこちらです。
今回分類しようとしているポエムは、いわゆる自然言語のテキストデータということになります。
画像データなどは、そのデータの持つ意味がダイレクトに各ピクセルのRGB値として表現されます。なので、画像データを（ある程度の前処理をした上で）そのまま分類器に突っ込めば、ひとまず分類することができます。
しかし、テキストデータとなると話は別で、コンピュータにとってのテキストデータとは即ち文字コードの集まりです。文字とそれに対応する文字コードの番号に意味はなく、単に管理するために機械的にあてがわれているわけです。というわけで、例えば「この文章、\u9231からUnicodeコードが1ずつ増えている！」とかそんな文字コードの番号で意味のあるテキストにはまずなっていません。
そこで必要になるのが、ある文書を意味を持つ最小単位である形態素へと分解し、形態素を足掛かりとして文章をベクトル化するという作業です。
こうした文書のベクトル化手法には、主に2種類のアプローチがあります。

* tfidfを代表とする形態素ひとつひとつを次元としたベクトル化手法
* doc2vecを代表とする分散表現を用いたベクトル化手法

で、今回のベクトル化手法ですが、これは分散表現によるベクトル化一択です。
理由は単純で、ポエムひとつひとつひとつが短く、またデータ数が少量であるためです。tfidfのような単語を次元とする手法でベクトル化してしまった場合、そもそも同じ単語が含まれておらず、共通する次元がなくまともに比較できないということが予想されます。その点分散表現では、同じ単語が含まれていなくても、単語の意味をある程度汲み取って比較することができます。なので、分類器が完成した後にイチローの名言をぶっ込んであげれば、見事にLOかBLEACHに分類できるわけです。

そして今回は分散表現の中でも、SWEMによるベクトル化を用いていきます。SWEMについて詳しく知りたい方は[こちらの記事](https://yag-ays.github.io/project/swem/)をどうぞ。

端的に説明すると、まず単語分散表現モデルを作成した上で、文書ベクトルのある次元の値を「文書に含まれる各単語のベクトルのうち最も特徴的な値」にするという手法です。最も特徴的意外にも、平均値にするなどあります。今回は、絶対値が最も大きな値を採用しました。

今回SWEMを採用した理由ですが、単語分散表現モデルさえあれば、新規文書のベクトル化が非常に簡単だったからです。今回の目的は、未知のポエムをLOかBLEACHに分類することです。そのためには、ただ与えられたテキストデータを文書ベクトルへと変換する必要があります。そんなわけで、手軽に新規文書をベクトル化できるSWEMがまさにうってつけでした。[^3]

### 単語ベクトルの準備

文書のベクトル化手法はめでたくSWEMに決まったわけですが、SWEMには単語モデルが必要です。しかし、一般人はそんなに都合よく学習済み単語モデルを持ってません。持ってませんよね？
そのためあえなく計画はここで頓挫……とはなりません。世の中には便利なものが意外と多く転がっています。今回利用させていただいたのは、[日本語 Wikipedia エンティティベクトル](http://www.cl.ecei.tohoku.ac.jp/~m-suzuki/jawiki_vector/)です。こちらは日本語版wikipediaを元に、word2vecで学習したモデルとなっています。形態素解析には新語対応もバッチリなmecab-ipadic-NEologdが使われているようです。

今回は記事名のベクトルは使わないので、[リリースページ](https://github.com/singletongue/WikiEntVec/releases)から20190520のjawiki.word_vectors.300d.txt.bz2を拝借しました。

そしてこれを解凍してpythonでモデル読み込み……としたところで事件発生です。はい、モデルが全然読み込み終わらない。待ってれば終わるかもしれないけど、文書ベクトルを作成しようとするたびに毎回毎回待ってはいられないです。というわけで、DBに単語ベクトルデータを打ち込みます。

<details><summary>mysqlへのベクトル挿入プログラム</summary><div>

```python3
import mysql.connector
from args import ARGS # mysqlへの接続設定


def create_table(cur, table_name, vec_size):
    print("create database")
    # mysql8.0デフォルトの設定では平仮名の濁点・半濁点を区別しないためCOLLATE=utf8mb4_binに設定
    sql = f"""
    CREATE TABLE IF NOT EXISTS {table_name} (
        word VARCHAR(255) NOT NULL PRIMARY KEY,
        {','.join([f"d{i} DOUBLE" for i in range(1,vec_size + 1)])}
    )
    COLLATE=utf8mb4_bin
    """
    cur.execute(sql)
    print("complete")
    return


def insert_vec(cur, vec_path, table_name, vec_size):
    insert_sql = f"""
    INSERT INTO {table_name} 
    (word, {','.join([f"d{i}" for i in range(1,vec_size + 1)])})
    VALUES ({' ,'.join(['%s' for i in range(vec_size + 1)])});
    """
    print('insert vector')
    with open(vec_path, 'r') as fi:
        header = fi.readline()
        insert_data = []
        for vec in fi:
            vec_val = vec.replace('\n', '').rsplit(' ', vec_size)
            insert_data.append(tuple(vec_val))

            if len(insert_data) >= 1000:
                cur.executemany(insert_sql, insert_data)
                insert_data = []

        else:
            if insert_data != []:
                cur.executemany(insert_sql, insert_data)
                del insert_data
    print("complete")
    return


if __name__ == "__main__":
    conn = mysql.connector.connect(**ARGS)
    cur = conn.cursor()

    table_name = "wiki_w2v_word"
    vec_size = 300
    create_table(cur, table_name, vec_size)
    insert_vec(cur, './model/jawiki.word_vectors.300d.txt',
               table_name, vec_size)

    cur.close()
    conn.commit()
    conn.close()

```

</div></details>


途中、mysql8.0だとデフォルトで日本語の濁点半濁点の有無を区別しない[^4]ことを知らなかったせいで盛大につまずきましたが、これで何とか単語ベクトルはデータベースに収まりました。

### 形態素解析

「単語ベクトルの用意ができたので、次はいよいよ文書ベクトルの作成です」と、いきたいところですが、その前にやらなければいけないことがあります。
英語と異なり日本語は単語ごとに半角スペースで分かれている、なんて便利なことにはなっていないので、学習やテスト、最終的に判別したいポエムに対して形態素解析を行う必要があります。本来は単語モデル作成前に行いますが、今回は学習済みモデルを用いたのでこの段階で用意します。利用した形態素解析器はMeCab、辞書はmecab-ipadic-NEologdです。ただし、日本語 Wikipedia エンティティベクトルの作成に用いられたNEologdとはバージョン[^5]が異なります。バージョンが異なると解析結果が異なる[^6]可能性があるため、本来であればバージョンも合わせるのが好ましいでしょう。が、今回はその手間を惜しみました。だって、面倒臭かったし……。

参考にしたのは[こちらの記事](https://kitayamalab.wordpress.com/2016/10/07/python%e3%81%a7%e5%bd%a2%e6%85%8b%e7%b4%a0%e8%a7%a3%e6%9e%90%ef%bc%8b%ce%b1/)です。

<details><summary>形態素解析用関数の作成プログラム</summary><div>

```python3:morphological_analysis.py
import MeCab


def morphological_analysis(m, text, use_original=False, POS_filter=[]):
    res = m.parseToNode(text)
    terms = []
    while res:
        term = [res.surface] + res.feature.split(',')
        if use_original:
            term[0] = term[7]
        if POS_filter != []:
            if term[1] in POS_filter:
                res = res.next
                continue
        terms.append(term[0])
        res = res.next
    terms = terms[1:-1]
    return terms


if __name__ == "__main__":
    m = MeCab.Tagger("mecabrc")
    m.parse('')
    text = "伏して生きるな、立ちて死すべし"
    print(morphological_analysis(m, text))


```

</div></details>

```console:実行結果
% python3 morphological_analysis.py
['伏して', '生きる', 'な', '、', '立ち', 'て', '死す', 'べし']
```


### 文書ベクトルの作成

今度こそ形態素解析の準備と単語ベクトルの用意ができたので、次はいよいよ文書ベクトルの作成です。実際のプログラムはこの後のsvmモデル作成も行っており、以下の折り畳みはそこからベクトル作成部分だけを抽出したものになっています。そのためファイル名が少々おかしいですがご了承ください。~~直すのが面倒だったんじゃ。~~


<details><summary>文書ベクトル（SWEM）の作成プログラム</summary><div>

```python3:create_svm_model.py
import csv
import numpy as np
import mysql.connector
import MeCab
from morphological_analysis import morphological_analysis
from database.args import ARGS


def fetch_word_vec(cur, word_list, table_name="wiki_w2v_word"):
    print("fetch word vec")
    sql = """
    SELECT * 
    FROM {table_name}
    WHERE word in ("{words}")
    """.format(table_name=table_name, words='","'.join([word.replace('"', '\\"') for word in word_list]))
    cur.execute(sql)
    vec_dic = {word[0]: word[1:] for word in cur.fetchall()}
    print("complete")
    return vec_dic


def create_swem_vec(vec_dic, words, vec_size=300):
    word_vectors = np.array(
        [vec_dic.get(word, [0 for i in range(vec_size)]) for word in words])
    max_vec = word_vectors.max(axis=0)
    min_vec = word_vectors.min(axis=0)
    doc_vec = [max_val if abs(max_val) > abs(
        min_val) else min_val for max_val, min_val in zip(max_vec, min_vec)]
    return doc_vec


def create_vector(cur, train_path):
    m = MeCab.Tagger("mecabrc")
    m.parse('')
    with open(train_path, 'r') as fi:
        reader = csv.reader(fi)
        word_set = set([])
        catch_copy_list = []
        for label, text in reader:
            words = morphological_analysis(m, text)
            catch_copy_list.append([label, words])
            word_set = word_set | set(words)
    vec_dic = fetch_word_vec(cur, word_set)
    catch_copy_vecs = [[label, create_swem_vec(
        vec_dic, words)] for label, words in catch_copy_list]
    return catch_copy_vecs



if __name__ == "__main__":
    train_path = "./catch_copy/train_data.csv"
    model_path = './model/UzuKou_v1.model'
    conn = mysql.connector.connect(**ARGS)
    cur = conn.cursor()

    catch_copy_vecs = create_vector(cur, train_path)

    cur.close()
    conn.close()


```

```text:./catch_copy/train_data.csv
LO,子供ですが何か？
LO,子供は世界の宝物
…
BLEACH,我等は　姿無きが故にそれを畏れ
BLEACH,人が希望を持ちえるのは死が目に見えぬものであるからだ
……

```

</div></details>

ひとまず初回は、文書を形態素へと分解するだけにします。定番の単語を原型に直す作業や、品詞によるフィルタリングは行いません。

### 分類器の作成

ここまでは分類以前のベクトル化でしたが、ここからはいよいよ分類器の実装です。さて、ひとえに分類手法といえど、その種類は様々あるわけです。線形回帰にロジスティック回帰、ランダムフォレスト回帰や、趣向を変えるとk近傍法などなど。多種多様な手法からどれを選ぶか？ですが、今回はあまり深く考えずにサポートベクターマシン(SVM)を用いました。これを選んだ理由は特にありません。唐突に雑になりましたが、実のところあまりこっちは詳しくありません……。

<details><summary>SVMモデル作成プログラム</summary><div>

```python3:create_svm_model.py
import csv
import pickle
import numpy as np
import mysql.connector
import MeCab
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
from morphological_analysis import morphological_analysis
from database.args import ARGS


def fetch_word_vec(cur, word_list, table_name="wiki_w2v_word"):
    print("fetch word vec")
    sql = """
    SELECT * 
    FROM {table_name}
    WHERE word in ("{words}")
    """.format(table_name=table_name, words='","'.join([word.replace('"', '\\"') for word in word_list]))
    cur.execute(sql)
    vec_dic = {word[0]: word[1:] for word in cur.fetchall()}
    print("complete")
    return vec_dic


def create_swem_vec(vec_dic, words, vec_size=300):
    word_vectors = np.array(
        [vec_dic.get(word, [0 for i in range(vec_size)]) for word in words])
    max_vec = word_vectors.max(axis=0)
    min_vec = word_vectors.min(axis=0)
    doc_vec = [max_val if abs(max_val) > abs(
        min_val) else min_val for max_val, min_val in zip(max_vec, min_vec)]
    return doc_vec


def create_vector(cur, train_path):
    m = MeCab.Tagger("mecabrc")
    m.parse('')
    with open(train_path, 'r') as fi:
        reader = csv.reader(fi)
        word_set = set([])
        catch_copy_list = []
        for label, text in reader:
            words = morphological_analysis(m, text)
            catch_copy_list.append([label, words])
            word_set = word_set | set(words)
    vec_dic = fetch_word_vec(cur, word_set)
    catch_copy_vecs = [[label, create_swem_vec(
        vec_dic, words)] for label, words in catch_copy_list]
    return catch_copy_vecs


def create_svm_model(vecters, model_path):
    X = [vec for label, vec in vecters]
    Y = [label for label, vec in vecters]
    clf = make_pipeline(StandardScaler(), SVC())
    clf.fit(X, Y)

    pickle.dump(clf, open(model_path, 'wb'))
    return


if __name__ == "__main__":
    train_path = "./catch_copy/train_data.csv"
    model_path = './model/UzuKou_v1.model'
    conn = mysql.connector.connect(**ARGS)
    cur = conn.cursor()

    catch_copy_vecs = create_vector(cur, train_path)
    create_svm_model(catch_copy_vecs, model_path)

    cur.close()
    conn.close()

```

</div></details>

SVMの実装には、scikit-learnを用いました。各パラメータは弄らず全てデフォルトのまま。真面目にやるならチューニングしていく必要がありますが、ひとまず初回なのでこれで良しとします。……まあそれ以前に、今回は学習データが貧弱であると言うこともあるので、あまりSVMのパラメータを真面目にいじったところでなぁというのも大きいです。

何はともあれ、これにてようやく__LO-BLEACHポエム分類器UzuKou_v1__が完成しました。

さあ、UzuKou_v1よ。その実力を見せておくれ……。

## 分類器の評価

評価に用いるテストデータは、前述の通りキャッチコピークイズで用いられた全30件のポエムです。わざわざクイズとして出題されただけあり、その全てがLOかBLEACHか人間でも判断しづらい難問揃い。果たしてUzuKou_v1は、その実力を示すことができるのでしょうか……？

<details><summary>評価用プログラム（キャッチコピークイズの一部ネタバレ注意）</summary><div>

```python3:test_model.py
import csv
import pickle
import mysql.connector
from sklearn.metrics import confusion_matrix
from sklearn.metrics import accuracy_score

from create_svm_model import create_vector
from database.args import ARGS


def test_model(model, vectors):
    preded_class = model.predict([vec for label, vec in vectors])
    true_class = [label for label, vec in vectors]
    print(confusion_matrix(true_class, preded_class, labels=["LO", "BLEACH"]))
    print(f'正解率: {accuracy_score(true_class, preded_class)}')
    return


if __name__ == "__main__":
    test_path = "./catch_copy/test_data.csv"
    model_path = f"./model/UzuKou_v1.model"
    print(model_path)
    model = pickle.load(open(model_path, 'rb'))
    conn = mysql.connector.connect(**ARGS)
    cur = conn.cursor()

    test_vecs = create_vector(cur, test_path)
    test_model(model, test_vecs)

    cur.close()
    conn.close()

```

```text:test_data.csv
LO,ただ、それだけのもの。
LO,お前には一生、勝てない気がする。
BLEACH,美しさとは、そこに何もないこと
BLEACH,伏して生きるな、立ちて死すべし
……
```

</div></details>

```terminal:test_model.py実行結果
% python3 test_model.py 
./model/UzuKou_v1.model
fetch word vec
complete
[[15  0]
 [10  5]]
正解率: 0.6666666666666666

```

混同行列の出力結果がちょっとわかりづらいので、下にまとめなおします。

||LOと予測|BLEACHと予測|
|---|---|---|
|本当にLO|15|0|
|本当にBLEACH|10|5|

まず正解率ですが、67%とぱっと見そこそこに見えます。そもそも人間が挑んだところで結構難しいクイズなので、6割近く正解できればなかなか良いのでは？と思います。__混同行列をみなければ__。

その混同行列ですが、こちらは予測結果と実際の分類を同時に表示し、予測の傾向をわかりやすく示すものです。では今回の混同行列をみてみると、明らかに変な傾向が出ています。
そうです、UzuKou_v1は__ほとんどのポエムをLOに分類__しているのです。
そもそもこのクイズ自体、LOとBLEACHのポエムが半々で混ざっているクイズであるため、極論全てのポエムをどちらか片方に予測したところで、正解率が50%出てしまうクイズなのです。そして現状では実に8割近くのポエムをLOと判定してしまっています。
これはあほあほAIといえど、流石に許容することができません。仮にこの分類器に未知のポエムを入れたところで、ほぼ全てのポエムをLOと答えることでしょう。何を聞いてもLOと答える分類器なんて求めていません。求めているのは、ポエムによってなんかそれっぽくLOとBLEACHを分類するAIなのです。

というわけで、この戦いは__延長戦__へと突入します。

# 分類器の改善

## どこを改善すべきか？

真のあほあほっぷりを露呈してしまったUzuKou_v1ですが、これを改善していくにはどうすれば良いでしょうか。
考えられる改善点は三つあります。

1. 学習データの見直し
2. ベクトル作成方法の見直し
3. 分類器のハイパーパラメータのチューニング

## 学習データの見直し（その１）

まずは学習データの見直しから行いましょう。
そもそも今回行うのは教師あり学習である以上、その教師にあたる学習データがあほあほでは、どれだけベクトル作成方法と分類器のチューニングをしたところで意味がありません。その点について考えると、今回は圧倒的にデータ数が少ないという改善のしようがない問題を抱えています。
ここはどうしようもありません。これを改善するとなれば、集英社および久保帯人先生に新しいポエムを量産していただけるようにお願いしつつ、LOはもっとたくさんポエムができるまで待ちましょう。それ以外にできることはありません。今は諦めましょう。

__とは、なりません__。
学習データですが、データ数が足りない以外にも実は現時点でとんでもない問題を抱えています。そして、その問題は現時点でも修正可能です。
その問題とは――

__めちゃくちゃ学習データ数がLOに偏っていることです__。
まず、現時点で用いている学習・テストデータの総数を確認してみましょう。

||LOポエム|BLEACHポエム|
|---|---|---|
|学習データ数|181|59|
|テストデータ数|15|15|
|データ総数|196|74|

はい、学習データの偏りが一目瞭然です。LOが181件に対し、BLEACHはたったの59件。データ数が少ないとはいえ、まさかの3倍以上も差がついています。流石にコミック単行本と月刊紙では、月刊紙に分があります。[^7]どう考えても、ほとんどLOに分類されていたことに小さくない影響を及ぼしていることでしょう。学習データが少なくなるのは少々痛いですが、LOに偏ることのデメリットの方が遥かに大きいので、LO側も学習データ数をBLEACHに合わせ、最新の59件分のデータ[^8]のみを用いることとします。

## 学習データの見直しによる改善（その１）

それでは、学習データの偏りをなくした上で、新たな分類器UzuKou_v2を作成します。学習データをいじっただけで、プログラムの変更はないのでそこは割愛します。

```console
 % python3 test_model.py
./model/UzuKou_v2.model
fetch word vec
complete
[[11  4]
 [ 8  7]]
正解率: 0.6
```

混同行列

||LOと予測|BLEACHと予測|
|---|---|---|
|本当にLO|11|4|
|本当にBLEACH|8|7|

さて、学習データを見直したことで正解率は落ちてしまいました。が、混同行列をご覧ください。
今回は、LOだけに分類してるわけではありません。ある程度しっかりとBLEACHにも分類しています。何でもかんでもLOに分類する分類器よりずっといい。よくやった、UzuKou_v2。ここからさらに分類器の精度[^9]を上げていきましょう。

## 学習データの見直し（その２）

学習データ自体に行えることはもうありません。
とは、なりません。自然言語処理において、普通は真っ先に行うべきことをまだ行っていないのです。そう、実は前処理を前処理がまだでした。むしろ、ここまで前処理を何もせずに来ていたことの方がおかしいレベルです。[^10]
ではどのような前処理があるかですが、以下のようなものがあるでしょう。

* 単語を原型に直す（表記揺れの統一）
* 文書の特徴なり得ない品詞の削除

まず単語を原型に戻す処理ですが、こちらは「走る」「走った（走っ・た）」「走れば（走れ・ば）」のような語を、全て「走る」に統一した上でベクトル化します。表記揺れをなくすことで、似た意味の文書はより似たものとして評価することができるようになります。もっとも、今回用いている日本語 Wikipedia エンティティベクトルでは、どうも学習の際に表記揺れの統一は行っていないようなので、どこまで効果があるかは未知数です。

次に品詞の削除ですが、SWEMの特性上「てにおは」のような助詞がある次元で文書ベクトルの値に採用されてしまう可能性があります。しかし、助詞は多くの場合文書の特徴とするのは不適切でしょう。他にも助動詞や副詞も同じ理由で削除します。
そして、記号も削除します。
というのも、ポエムを収集して集めている際に気づいたのですが、「。！」があった時点でBLEACHの可能性が消えるからです。また、逆にスペースが入った場合はBLEACHである可能性が跳ね上がります。
正直これがベクトルにどこまで影響しているかは不明[^11]です。しかし、少なくともLOとBLEACHのポエムを句点の有無なんてしょうもない方法で判別するAIなんて求めていません。[^12]頭の良い人たちが考えた高度な技術で、アホなことをするAIがみたいからこんな面倒なことをしているのです。

というわけで、行う処理はこちらです。

* 単語を原型に戻す
* 記号、副詞、助詞、助動詞の削除

幸いにも元から原型に戻す処理、品詞のフィルタリング機能は関数に実装しているため[^13]、呼び出し部分で引数を追加するだけで十分です。

```python3:create_svm_model.py
def create_vector(cur, train_path):
    m = MeCab.Tagger("mecabrc")
    m.parse('')
    with open(train_path, 'r') as fi:
        reader = csv.reader(fi)
        word_set = set([])
        catch_copy_list = []
        for label, text in reader:
            # ここに原型に戻す処理と品詞のフィルタリングを追加した
            words = morphological_analysis(m, text, use_original=True, POS_filter=["記号", "副詞", "助詞", "助動詞"])
            catch_copy_list.append([label, words])
            word_set = word_set | set(words)
    vec_dic = fetch_word_vec(cur, word_set)
    catch_copy_vecs = [[label, create_swem_vec(
        vec_dic, words)] for label, words in catch_copy_list]
    return catch_copy_vecs
```

## 学習データの見直しによる改善（その2）

それでは、前処理による改善を行ったUzuKou_v3の結果についてみていきましょう。

```console
 % python3 test_model.py
./model/UzuKou_v3.model
fetch word vec
complete
[[13  2]
 [ 8  7]]
正解率: 0.6666666666666666
```

混同行列

||LOと予測|BLEACHと予測|
|---|---|---|
|本当にLO|13|2|
|本当にBLEACH|8|7|

若干ですが、正解率が上がりました。混同行列をみる限り若干LOに偏ってしまっていますが、それでも最初のUzuKou_v1よりは明らかに偏りが少なく良い結果とみて良いでしょう。

なぜLO側に多く判別されてしまうかについての予想ですが、BLEACH側のポエムの方がBLEACHらしいポエムが多いから、ということが考えられます。というのも、LOとBLEACHのポエムは確かにどちらかわかりづらいポエムもある一方、ど素人がみてもあからさまに判別できるポエムが両方にあります。そして、BLEACHはBLEACHっぽいポエムが多い（ように見えます）。
この辺りは真面目に調べたわけでもなく完全に感覚ですが、LOとBLEACHの判別がつきにくいポエムはLO側に多いことが、LOに多く分類されてしまう理由の仮説として考えられます。[^14]

## ベクトル作成方法の見直し・分類器のハイパーパラメータのチューニングチューニング

さて、学習データの見直しも終わったことなので、次はベクトル作成方法の見直しや分類器のハイパーパラメータのチューニングをしていきましょう。
と、ここからさらにUzuKouを改善していく予定でしたが……ここで残念なお知らせです。
この辺りで私が完全に__正気に戻ってしまった__ため、あほあほAI作成のモチベーションが一気にゼロに近づきました。
それでは次回作にご期待ください。

# イチローの名言をLOとBLEACHに分類する

いや、流石にそこで力尽きるのはないと思う。
なので、最後の力を振り絞り、このプロジェクトの本来の目的を達成していこうと思います。

<details><summary>LO-BLEACH分別器のプログラム</summary><div>

```python3:LO_BLEACH.py
import sys
import csv
import pickle
import mysql.connector

from create_svm_model import create_vector
from database.args import ARGS

if __name__ == "__main__":
    text_path = "./catch_copy.txt"
    model_ver = input("input model version: ")
    model_path = f"./model/UzuKou_v{model_ver}.model"

    model = pickle.load(open(model_path, 'rb'))
    conn = mysql.connector.connect(**ARGS)
    cur = conn.cursor()

    text_vecs = create_vector(cur, text_path)
    preded_class = model.predict([vec for label, vec in text_vecs])

    cur.close()
    conn.close()

    with open(text_path, "r") as fi:
        reader = csv.reader(fi)
        text_list = [text[1] for text in reader]
        for p_class, text in zip(preded_class, text_list):
            print("==========================")
            print(text)
            print(p_class)
```

```text:catch_copy.txt
,パワーは要らないと思います。それより大事なのは 自分の『形』を持っているかどうかです
,決して、人が求める理想を求めません。人が笑ってほしいときに笑いません。自分が笑いたいから笑います
,自分のできることをとことんやってきたという意識があるかないか。それを実践してきた自分がいること、継続できたこと、そこに誇りを持つべき
,特別なことをするためには普段の自分でいられることが大事です
,僕は天才ではありません。 なぜかというと自分が、 どうしてヒットを打てるかを 説明できるからです
```

</div></details>

catch_copy.txtに判別したいポエムを記述し、あとはUzuKouに全てを委ねます。今回用いるUzuKou_v4は、上記の改善を全て行った上で、これまでテストデータとして利用してきたデータも学習データに含めた[^15]完全体です。

```text:判別対象
1. パワーは要らないと思います。それより大事なのは 自分の『形』を持っているかどうかです （卯月コウ未判定）
2. 決して、人が求める理想を求めません。人が笑ってほしいときに笑いません。自分が笑いたいから笑います　（卯月コウ判定：LO）
3. 自分のできることをとことんやってきたという意識があるかないか。それを実践してきた自分がいること、継続できたこと、そこに誇りを持つべき　（卯月コウ判定：BLEACH）
4. 特別なことをするためには普段の自分でいられることが大事です　（卯月コウ判定：LO）
5. 僕は天才ではありません。 なぜかというと自分が、 どうしてヒットを打てるかを 説明できるからです　（卯月コウ判定：イチロー）
```

さて、判定結果は

```console
% python3 LO_BLEACH.py
input model version: 4
fetch word vec
complete
==========================
パワーは要らないと思います。それより大事なのは 自分の『形』を持っているかどうかです
BLEACH
==========================
決して、人が求める理想を求めません。人が笑ってほしいときに笑いません。自分が笑いたいから笑います
LO
==========================
自分のできることをとことんやってきたという意識があるかないか。それを実践してきた自分がいること、継続できたこと、そこに誇りを持つべき
BLEACH
==========================
特別なことをするためには普段の自分でいられることが大事です
LO
==========================
僕は天才ではありません。 なぜかというと自分が、 どうしてヒットを打てるかを 説明できるからです
LO
```

さすがだ、UzuKou_v4……。本物の卯月コウが判定したところは全部合ってるぞ……。システム外のイチロー判定は無理だったけど。

# おわりに
最後まで読まれた方、お疲れ様でした。
途中を飛ばして結果だけ読んでいる方、かしこい。

切り抜き動画をみて変な天啓を得てしまったが故に、ここまで暴走し切ってしまいました。この記事を書くために土日が消えました。正気に戻ったのに、記事を書いている途中でまたあの狂気が少し戻ってきていたようです。怖いですね。
ここまでやった結果私が手に入れたのは、ポエムをひたすらLOとBLEACHに分類するだけのAIです。虚しいです。この虚しさと引き換えに何かを手に入れた気がする云々と書こうかとも思いましたが、どう考えてももっと有意義な休日の使い方があったなぁと後悔しか湧き出てこないので虚無です。

でも、人はそうとわかっていても、時にあほあほAIを作りたくなります。滑るとわかっていても、アホな思いつきの原動力は、時にいかなる責任よりも人を突き動かします。
もしこれを読むみなさんの中に、同じようにあほあほな目的のために作ったAIがあれば、教えてくださると幸いです。

# おまけ

```console
% python3 LO_BLEACH.py
input model version: 4
fetch word vec
complete
==========================
いつから朝でどこから友達なの？
LO
```


# 参考ページ
* [2.0お披露目配信でイチローの名言をLOとBLEACHに分類する卯月コウ
](https://www.nicovideo.jp/watch/sm36026037)
* [『BLEACH』と『コミックLO』のポエムが似ていると話題　超難度の「出典当てクイズ」に僕はついてゆけるだろうか - ねとらぼ](https://nlab.itmedia.co.jp/nl/articles/2006/05/news107.html)
* [COMIC LOの表紙キャッチコピーの一覧とは (コミックエルオーノヒョウシキャッチコピーノイチランとは) [単語記事] - ニコニコ大百科](https://dic.nicovideo.jp/a/comic%20lo%E3%81%AE%E8%A1%A8%E7%B4%99%E3%82%AD%E3%83%A3%E3%83%83%E3%83%81%E3%82%B3%E3%83%94%E3%83%BC%E3%81%AE%E4%B8%80%E8%A6%A7)
* [【BLEACH】表紙・ポエムまとめ【全74巻】](https://dic.nicovideo.jp/a/comic%20lo%E3%81%AE%E8%A1%A8%E7%B4%99%E3%82%AD%E3%83%A3%E3%83%83%E3%83%81%E3%82%B3%E3%83%94%E3%83%BC%E3%81%AE%E4%B8%80%E8%A6%A7)
* [BLEACH、コミックLOキャッチコピークイズ](https://docs.google.com/forms/d/e/1FAIpQLSf0gx7_HzL4BS8ZZhhUjUhel2Thwl5hKq_1f-cst4klgyZ7lw/viewform)
* [[Python]文書分類における文書ベクトル表現手法の精度比較 - Qiita](https://qiita.com/nekoumei/items/1f5ec09e422a4be99810)
* [SWEM: 単語埋め込みのみを使うシンプルな文章埋め込み - Out-of-the-box](https://yag-ays.github.io/project/swem/)
* [日本語 Wikipedia エンティティベクトル](http://www.cl.ecei.tohoku.ac.jp/~m-suzuki/jawiki_vector/)
* [MySQL 8.0ではデフォルトで濁点半濁点を区別しなくなる - かみぽわーる](https://blog.kamipo.net/entry/2017/06/22/005958)
* [Pythonで形態素解析＋α | kitayama lab](https://kitayamalab.wordpress.com/2016/10/07/python%e3%81%a7%e5%bd%a2%e6%85%8b%e7%b4%a0%e8%a7%a3%e6%9e%90%ef%bc%8b%ce%b1/)
* [機械学習に知っておくべき10のアルゴリズム | Octoparse](https://www.octoparse.jp/blog/10-machine-learning-algorithms-you-should-know/)
* [sklearn.svm.SVC &mdash; scikit-learn 0.23.1 documentation](https://scikit-learn.org/stable/modules/generated/sklearn.svm.SVC.html)
* [Python scikit-learnで機械学習モデルを保存＆ロードする - 株式会社ロカラボ](https://localab.jp/blog/save-and-load-machine-learning-models-in-python-with-scikit-learn/)
* [scikit-learn でクラス分類結果を評価する &#8211; Python でデータサイエンス](https://pythondatascience.plavox.info/scikit-learn/%E5%88%86%E9%A1%9E%E7%B5%90%E6%9E%9C%E3%81%AE%E3%83%A2%E3%83%87%E3%83%AB%E8%A9%95%E4%BE%A1)
* [抑えておきたい評価指標「正解率」「精度」「再現率」 - HELLO CYBERNETICS](https://www.hellocybernetics.tech/entry/2017/02/25/015340)
* [形態素解析ツールの品詞体系](http://www.unixuser.org/~euske/doc/postag/)
* [イチロー：モチベーションの上がる名言121選](https://motiv.top/word/ichiro/)

[^1]: 切り抜き元ではなくニコ動側のコメント。
[^2]: 単に正気を失ったとも。
[^3]: 他にも、doc2vecで文書ベクトルを一から学習するにはデータが少ないというのもある。SWEMは学習済みの単語モデルさえあれば良いので便利だ。むしろそっちがメインの理由では。
[^4]: 以前からデフォルトでは英単語の大文字小文字は区別していないらしいので、どちらにせよ躓く定めだった。
[^5]: 今回用いたのはRelease 20200130-01のもの。
[^6]: 特に問題なのが後のバージョンで追加された新語で、これはどう探してもベクトル自体が見つからなくなってしまう問題がある。たとえば、今回の単語モデルに「にじさんじ」の単語は存在していない。もっとも、この例は学習に用いた記事の問題であるかもしれないが。
[^7]: ちなみにLOの創刊号は2002年9月20日発行、BLEACHの単行本１巻発売日は2002年1月5日らしい（wikipediaより）。
[^8]: テストデータに用いられている分は除いた上で、新しい59件分を用いる。
[^9]: ここで精度といっているが、機械学習の評価指標である「精度」とは異なる。日本語的なニュアンスで受け取って欲しい。ちなみにここでいわゆる精度、再現率、F値などを用いて評価していないのは、LOとBLEACHそれぞれにフォーカスして評価するのが面倒だったからである。分類器を評価し始めた辺りから徐々に正気に戻り始めていたので仕方がない。
[^10]: ぶっちゃけると実は学習データの見直しをする前に前処理を実装していた。というのも、記事にするにあたり効果の高い学習データの見直しを先に行ったことにした方が面白そうと思ったからである。記事ではまるで元から学習が偏っていた原因がわかっていた、または改善を順序立てて行ったような書き方をしているが、実際は超行き当たりばったりである。
[^11]: 実はわかる。作成されたSWEMベクトルをみて、どの次元でどの単語の値が採用されたのかは単語ベクトルと比較すればあっさりわかってしまう。が、やっぱり面倒くさかった。ちなみにこれを利用し、文書中の特徴語として抽出することもできたりする。
[^12]: 真面目に判別したいだけの場合、それで構わないならスマートな方法を取るべきというのはよく言われる。今回の場合、真面目に考えたとしても無しだろう。私たちがLOとBLEACHの特徴を判別する時、気にかけるのは句点の有無ではないはずだ。
[^13]: 嘘である。実際はこのタイミングで初めて実装したが、記事を書くために元のバージョンを探すのが面倒だった。
[^14]: この辺りの問題は、作成されたベクトルをPCAによって次元圧縮して可視化したり、ベクトル同士の類似度を調べれば、ある程度裏付けできる可能性もある。一応どちらもプログラムとしては実装したのだが、真面目に分析しようかと思ったあたりで完全に正気に戻ってしまったので頓挫している。というか実はこの記事、作成したベクトルの正統性について何も検討を行っていない。何だこのクソ記事は。
[^15]: 実際にこれをやった場合、これまでの評価の意味はかなり薄れてしまう。本来なら禁じ手である。が、この後の判別結果はこの方法で作成した分類器の結果の方が良かったので採用。エンタメ性を重視した。