textCNNを可視化（前半）

#0.背景
textCNNは、画像認識に使うCNNをテキスト分類に使えないか、ということで考え出されたCNN。
textCNNといえば[この論文](https://www.researchgate.net/publication/265052545_Convolutional_Neural_Networks_for_Sentence_Classification)！
[引用数もすごい](https://scholar.google.com/citations?user=n_ts4eYAAAAJ&hl=en)。

これを、[TensorFlow2.0で実装したもの]( https://github.com/ShaneTian/TextCNN.git)があったので、日本語（の特許文書）に適用すべく借りて少しいじってみました。
目的としては、分類問題でtextCNNが着目する素性と、randomforestが注目する素性（情報利得使うはず）との違いを見てみたい、という所から。
ちょうど、（普通の）[CNNの中間層とフィルタを可視化する記事](https://ohke.hateblo.jp/entry/2019/03/30/230000)があったので、ここも参考に（記事後半）。



#1.工夫点
上記のコードは、推測ですが中国の裁判例を調べて、文章からどのタイプの案件かを特定する、ということをするためのもの。
data_helper.pyの一部

    case_type = ['民事案件', '刑事案件', '行政案件', '赔偿案件', '执行案件']

その辺から修正を始めて、data_helper.pyの他に、train.py、text_cnn.pyの３つがあり、それぞれ目的に応じて修正。

上記のコードからの主要な変更点(工夫点)は以下のとおり

## 前処理部分（data_helper.py）
⇒デフォルトだと文字単位でくぎってたので、ちょっといやだなと思い、
mecab（janome利用）で1gram化⇒途中でngramとか特許分類とか混ぜたくなった時のことを考え、投入前のデータベース段階で、素性をパイプで区切ったデータに変更（サボり）。


data_helper.py（の一部）

        #def text_preprocess(text):
            #もともとのやつ↓
            #text = re.sub(r"[\d+\s+\.!\/_,?=\$%\^\)*\(\+\"\'\+——！:；，。？、~@#%……&*（）·¥\-\|\\《》〈〉～]","", text)
            #text = re.sub("[<>]", "", text)
            #text = re.sub("[a-zA-Z0-9]", "", text)
            #text = re.sub(r"\s", "", text)
            #if not text:
            #    return ''
            #return ' '.join(string for string in text)
             
            #日本語用に1gram
            #token_sen1 = []
            #for token in t.tokenize(text):
            #    partOfSpeech = token.part_of_speech.split(',')[0]
            #    if partOfSpeech == u'名詞':
            #        token_sen1.append(token.surface)
            #    else:
            #        pass

            #結局|区切りのデータを投入してごまかす。
            return ' '.join(text.split("|"))


## embedding層をone-hotに
ここがミソでしたが、text-cnnのembedding層をone-hotにしておかないと、フィルターを可視化した際に、どの言葉をCNNは注目してるのか？が分かりにくくなるため。
デフォルトでは、keras.layers.Embeddingを使ってたのでコメントアウトして、one-hotに書き換え。

    def OneHot(input_dim=None, input_length=None):
        # Check if inputs were supplied correctly
        if input_dim is None or input_length is None:
            raise TypeError("input_dim or input_length is not set")

        # Helper method (not inlined for clarity)
        def _one_hot(x, num_classes):
            return K.one_hot(K.cast(x, 'uint8'),
                              num_classes=num_classes)

        # Final layer representation as a Lambda layer
        return Lambda(_one_hot,
                      arguments={'num_classes': input_dim},
                      input_shape=(input_length,))

       ・・・・略・・・

        embed_initer = keras.initializers.RandomUniform(minval=-1, maxval=1)
        #embed = keras.layers.Embedding(vocab_size, embed_size,
        #                               embeddings_initializer=embed_initer,
        #                               input_length=feature_size,
        #                               name='embedding')(inputs)
        embed = OneHot(input_dim=vocab_size,input_length=feature_size)(inputs)
                                   
        # single channel. If using real embedding, you can set one static
        embed = keras.layers.Reshape((feature_size, embed_size, 1), name='add_channel')(embed)




## callback関数のearlystoppingの部分
    tb_callback = tf.keras.callbacks.TensorBoard(os.path.join(args.results_dir, timestamp, 'log/'),
                                              histogram_freq=0.1, write_graph=True,
                                              write_grads=True, write_images=True,
                                              embeddings_freq=0.5, update_freq='batch')
    earlystop_callback = tf.keras.callbacks.EarlyStopping(monitor='val_accuracy', min_delta=0.0001, patience=1)                                           

callbackが２個ある場合どうするんだろう？と思ったら、

    history = model.fit(x=x_train, y=y_train, batch_size=args.batch_size, 
    epochs=args.epochs,steps_per_epoch = 100
                        ,callbacks=[tb_callback,earlystop_callback], 
    validation_split=args.fraction_validation, shuffle=True)

と、callbacks=[tb_callback,earlystop_callback]で配列で指定すればOK。

など。


# 2.出力結果
とある技術分野に関する、タイトル、要約、請求項を1gramにしたデータを投入＆実行。

    !python train.py -p=128 -c=2 -n=100 -f=3,4,5

以下ダラッと出力結果。

    Parameters: Namespace(batch_size=64, dropout_rate=0.5, embed_size=512, epochs=10, 
    filter_sizes='3,4,5', fraction_validation=0.05, num_classes=2, num_filters=100, 
    padding_size=128, regularizers_lambda=0.01, results_dir='./results/', 
    test_sample_percentage=0.1) 

    ['エアバッグ|用|ガス|発生|器|有|車輌|火災|時|ハウジング|加熱|確実|とらえ|ガス|発生|器|・・・略・・・']
    Write train data to ./data/train_data.csv ...
    Write test data to ./data/test_data.csv ...
    Loading data from ./data/train_data.csv ...
    Vocabulary size: 4548
    Shape of train data: (599, 128)
    
    ・・・略・・・
     
    Model: "model"
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/385220/91f2e00b-e59d-3cfd-2603-1d15c07fd09f.png)




 ・・・以下略・・・


#3.感想
モデルは無事に出来たので、後半で中間層とフィルターを可視化します！！
⇒[後半](https://qiita.com/niship2/items/3003584c4ecf5290fd5c)

もとの論文では、filterがstaticなものとdynamicなもので２つ使っているようですが、参考にしたコードでは実装されておらず。現状そこまで変更できませんでした。
ちなみに、textcnnで調べると中国語の情報が多い。中国語に適しているなど何かあるのかな。
こういうのkerasを使いつつも、サクッと作れるとかっこいいんでしょうねぇ－