半教師ありLDAで漫画の文章解析してみた。

##はじめに
一般的なLDAではトピック割り当ては自動で行われます。それは良い場合もありますが、悪い場合の方が個人的には大きいと思います。
何故ならば期待した通りに正しいトピックに分かれてくれないなどの問題があります。
それを解決するのが半教師ありLDAです。

##LDAとは
Latent Dirichlet Allocationの略称であり、日本語では潜在的ディリクレ配分法といいます。
主に自然言語処理での文章分類に使われることが多いように思います。（画像のキャプション分類などでも使われることもあります。）
詳しくは[こちら](https://whispon.com/wp/wp-content/uploads/2018/01/s1412501oshita-9-1.pdf)を参考にしてください。

## GuidedLDAとは
先に述べたように一般的なLDAでは自動でトピックが分かれてしまいます。いいこともある反面、実運用では不自由なことも少なくありません。
では、そこで一つのアイデアとして、もしも自分でトピックの一部を教えることができたらどうでしょう？
そういうアイデアの元、Vikash Singhらが開発したライブラリが[GuidedLDA](https://github.com/vi3k6i5/guidedlda/)です。

## 使い方
実装の動機などは参考文献を参考にしてもらうとして早速、漫画解析を行います。
<img src="https://qiita-image-store.s3.amazonaws.com/0/33329/0c98ec66-8f09-0d09-4aea-5917bed0a3e0.jpeg" width=30%>

分析するのはハイキューの18巻です。（何らかの手法で漫画の文章の単語を入手してください。）

LDAでは文単位で解析します。そこで問題になるのがどこで文を区切るかです。今回はシーンを人海戦術で推定し、シーンごとに一つの文としました。


教師データは以下のように指定します。
ルールとしてはTopic0が固有名詞ぽいもの,Topic1がバレーボールの技、Topic2が漫画とは関係ないクレジット,Topic3が応援などの擬音語としています。
これは、なんどもいいますが、このように自分で分類したいというモチベーションからこのように教師データを設計しています。

```kyoushi.py
# 教師データ
seed_topic_list =[['白布', '大平', '白鳥', '烏野高校', '白鳥沢学園','日向', '烏野', '五色', '白布', '影山', '西谷'],
                  ['脚本','監督','編集','デザイン','表紙','撮影','音楽','美術監督','pages','comics','extra','jump'],
                  ['ナイス','アウト','ナァーイス','セッター','ブロック','トス','レシーブ','サーブ']]
```

文章を文書ごとに頻度ベクトル化します。

```hindo_to_vector.py
# 文書の頻度をベクトル化する必要があるため。一連の処理    
from sklearn.feature_extraction.text import CountVectorizer
vectorizer = CountVectorizer(min_df=1)
X = vectorizer.fit_transform(text_all_kanma)
vocab = vectorizer.get_feature_names()
word2id = dict((v, idx) for idx, v in enumerate(vocab))
```

モデル化は以下のように行います。

```to_model.py
model = guidedlda.GuidedLDA(n_topics=3, n_iter=1500, random_state=random_state, refresh=20)
model.fit(X, seed_topics=seed_topics, seed_confidence=0.8)
```

文章の単語をトピックに割り当てた結果は以下のように確認します。

```kakunin.py
# トピックの幾つの代表的な単語を表示するかの数
n_top_words = 30
topic_word = model.topic_word_
for i, topic_dist in enumerate(topic_word):
    topic_words = np.array(vocab)[np.argsort(topic_dist)][:-(n_top_words+1):-1]
    print('Topic {}: {}'.format(i, ' '.join(topic_words)))
```

出力のサンプルです。

```print.py
Topic 0: 牛島 日向 さん 烏野 天童 ws 五色 うしじま 白布 西谷 アウト ひなた 川西 山形 白鳥 ナイス out ネット mb 月島 にしの in 2年 感じ 第2セット 大地 からす 返せ てる 1年
Topic 1: 3年 能力 しら 烏野 もの コミックス 白鳥沢学園 ほん 古舘春一 自分 倒し セッター パワー 段階 当時 若利 表記 大平 発売 extra pages 集英社 なり パラメータ comics jump 全国 11 ナァーイス スタミナ
Topic 2: ブロック 白鳥 とり する 影山 5番 トス サーブ いる 攻撃 いっ スパイク しら ナァーイス なっ www ホント なん れる これ ゲス 読み 止め 意味 おれ かげ 高野 考え 行ける tttt
```

データが少ないので、精度がイマイチなもののなんとなく分かれていることがわかります。
普通のLDAでやると自分でトピックを決められないので大きな一歩です。

また、単一の文がどのトピックから構成されているのかは以下のようにやります。
gensimではget_document_topicにあたります。

わかち書きして渡してあげてください。

```single.py
from sklearn.feature_extraction.text import CountVectorizer
X = vectorizer.transform(['烏野高校 サーブ イン'])

doc_topic = model.transform(X[0,:])
print(doc_topic)
```

```出力.py
[[0.69232385 0.00816821 0.29950794]]
```

Topic0が0.69と強く反応していることがわかります。この文はTopic0に属していると分かります。


```single2.py
Y = vectorizer.transform(['白鳥沢学園 烏野高校'])

doc_topic = model.transform(Y)
print(doc_topic)
```

```出力2.py
[[2.49410508e-03 9.97489824e-01 1.60704229e-05]]
```

Topic1が約0．97なのでTopic1に属していることがわかります。

```single3.py
Y = vectorizer.transform(['集英社　出版'])

doc_topic = model.transform(Y)
print(doc_topic)
```

```出力3.py
[[0.00133662 0.99701572 0.00164766]]
```

架空のそれっぽい文章でも、LDAはベイズモデリングなので存在していないけど、それっぽい文章でもいけます。

```single4.py
Y = vectorizer.transform(['ちゃんと サーブ　しろよ'])

doc_topic = model.transform(Y)
print(doc_topic)
```

```出力4.py
[[8.98343374e-04 1.33751698e-03 9.97764140e-01]]
```

ちゃんとTopic2に分類されてますね。

### 全体のソースコード

```bunseki.py
import MeCab
mecab = MeCab.Tagger ('-d /usr/local/lib/mecab/dic/mecab-ipadic-neologd')

def get_meishi(document, stopwords):
    meishi = []
    mecab.parse("")
    node = mecab.parseToNode(document).next
    stopwords = stopwords
    while node:
        nodeFeature = node.feature.split(",")
        # 名詞のみ取得
        if nodeFeature[0] == "名詞":
            node_surface = node.surface 
            flag = False
            for stop in stop_words:
                if stop == node_surface:
                    flag = True
            if flag != True:
                meishi.append(node_surface)
        node = node.next
    return meishi


import numpy as np
import guidedlda

with open("ocr_haikyu.txt", "r") as f:
    texts = f.readlines()

text_all = []
tmp = []

stop_words = ["で", "？", "も", "は", "か", "た", "て", "の", "し", "だ", "よ","０","ん","な","！","…","ー","に","お","ぜ","ここ","こと","は"]

# 改行コード区切りで一つの文章としている。
for text in texts:
    tmp.append(get_meishi(text, stop_words))
    if text == '\n':
        text_all.append(sum(tmp, []))
        tmp=[]
        
text_all_kanma = []
tmp = ""
for text in text_all:
    for i,t in enumerate(text):
        # 最初にスペースが入らないようにするため
        if i==0:
            tmp = t
        else:
            tmp = tmp + " " + t
    text_all_kanma.append(tmp)
    tmp = ""

# 文書の頻度をベクトル化する必要があるため。一連の処理    
from sklearn.feature_extraction.text import CountVectorizer
vectorizer = CountVectorizer(min_df=1)
X = vectorizer.fit_transform(text_all_kanma)
vocab = vectorizer.get_feature_names()
word2id = dict((v, idx) for idx, v in enumerate(vocab))

# 教師データ
seed_topic_list =[['白布','大平','白鳥','烏野高校','白鳥沢学園'],
                  ['ブロック','トス','レシーブ','サーブ'],
                  ['脚本','監督','編集','デザイン','表紙','撮影','音楽','美術監督','pages','comics','extra','jump'],
                  ['ナイス','アウト','ナァーイス']]
seed_topics = {}
for t_id, st in enumerate(seed_topic_list):
    for word in st:
        seed_topics[word2id[word]] = t_id

import random
# 毎回違った値を出力するため
random_state = random.randint(1,100)
print("random_state", random_state)
model = guidedlda.GuidedLDA(n_topics=4, n_iter=1500, random_state=random_state, refresh=20)
model.fit(X, seed_topics=seed_topics, seed_confidence=0.8)

# トピックの幾つの代表的な単語を表示するかの数
n_top_words = 15
topic_word = model.topic_word_
for i, topic_dist in enumerate(topic_word):
    topic_words = np.array(vocab)[np.argsort(topic_dist)][:-(n_top_words+1):-1]
    print('Topic {}: {}'.format(i, ' '.join(topic_words)))
```

##考察
本記事では、一つの漫画のみの検証となっているが、複数の漫画をそれぞの文として半教師ありLDAでトピック割り当てを行った。結果としてはトピックの解釈が非常に困難だった。
そこで、漫画に書いてある目次の情報(1話、２話...)などを利用して、話ごとに文として解析を行った。なぜこのように解析をするかといえば、LDAでは暗黙的に同様の分布から似たような単語が生成されるような文書を暗に仮定しているからである。一巻分を一つの文とした場合その仮定が破綻する。
実際の応用ではまずは教師なしLDAから始め、トピックを解釈しトピックの代表的な単語を抽出し、そのあとにその単語を教師データとして半教師ありLDAでトピック割り当てすることが望ましいと考えられる。どのような文書を用いるか、また漫画の場合はOCRの性能に学習の結果が左右されがちな傾向も手打ちの文章と比較して見られた。
また、話ごとに一つの文とするという部分も、例えば漫画の場面を画像認識で認識し、シーンごとにLDAを行うなどの工夫が必要だと思われる。

参考文献

[How our startup switched from Unsupervised LDA to Semi-Supervised GuidedLDA](https://medium.freecodecamp.org/how-we-changed-unsupervised-lda-to-semi-supervised-guidedlda-e36a95f3a164)
Vikash Singh

[GuidedLDA](https://github.com/vi3k6i5/guidedlda/)
Vikash Singh