DCGANでテキスト生成を試してみた2

こんにちは、Link-Uの町屋敷です。

前回は、Word2Vec(以下W2V)とDCGANで英語のテキストを生成しているサイトがあったので、日本語でも試しよう!と言う事で実装してみたところなかなか残念な結果だったので、今回はもう少しなんとかならないか続きをします。

生成結果を視覚化する

結果を良くするにはパラメーター調整をしていかなければならないんですが、画像と違って文章生成の場合生成された結果が直感でわかりにくくなっています。

そこで、わかりやすくするためにジェネレーターが生成した結果をWord2Vecで文章に戻すのではなく、それを次元圧縮してプロットします。

    def show_tsne(self, epoch, real, fake, save_pic):
        from sklearn.manifold import TSNE
        col = np.zeros(len(real))
        col = np.append(col, np.ones(len(fake)))
        
        comp = TSNE(n_components=2, random_state=0).fit_transform(np.reshape(np.vstack((real,fake)), (-1, self.img_cols * self.img_rows * self.channels)))
        plt.scatter(comp[:, 0], comp[:, 1], c=col)
        plt.xlim(-30,30)
        plt.ylim(-30,30)
        if save_pic:
            plt.savefig("images/sent_gan_%d.png" % epoch)
        else:
            plt.show()
        plt.close()

本物の文章と偽物の文章を受け取ってTSNEで圧縮してプロット。今回は本物を紫、偽物を黄色でプロットする

いい偽物の文章が生成されていれば、2色のプロットは混ざり合うが、そうでなければ分離される。

ためしに前回の最終結果で試してみるとこんな感じ。

Epoch 100

Epoch 1000

完全に分離されているのがわかります。

MeCabからの品詞の情報を使う

上記の確認方法を使ってDやGのモデル、W2Vのベクトルの長さ、学習率などのパラメーターをいろいろいじくり回しましたが良い結果にはなりませんでした。

出力された結果を見てみると、品詞がガバガバになっていたので、MeCabから品詞の情報を抜き出して、W2Vの後ろに追加したら多少マシになるんじゃないたと思って追加しました。

    def AnalyzeWord(self, word):
        gen = self.mecab.parse(word, as_nodes=True)
        for w in gen:
            return w.posid, w.surface

品詞はposidから取得できます。posidさえあれば単語の活用は単語の原型から変形させればいいので、簡単のために入力する単語をすべて原型にしました。

結果はこんな感じ。

Epoch 100

Epoch 1000

多少マシなはなったがまだひどい。

ちなみに生成された文章はこんな感じ。

掘り起こしぎゅれんずつ欄オアフまた。。

掘り起こしぎゅれんずつプロバスケットボールリーグオアフまた。。

掘り起こしぎゅれんずつシティーハンタースペシャルオアフまた。。

最後には。がつくっていうのくらいしか学習できてない感がある。

品詞の並びを生成してそれを単語に変換する

品詞の並びを生成する

流石にこのままでは終われないので作戦変更(妥協)。

直接雑音から文章を生成するのを諦め、DCGANと2つ使った文章の生成を目指す。

1つめのDCGANで雑音から品詞の列を生成するジェネレーターを生成し、

品詞を単語に変換するニューラルネットを2つめのDCGANで生成することを目指す。

まず雑音から品詞の列を生成する。これが出来ないなら話にならない。

今までW2Vを入力していたところの先程の品詞の列のみを入力してDCGAN。

Epoch 100

Epoch 300

Epoch 500

Epoch 1000

Epoch 5000

どうやら品詞の列ぐらいは生成できるようだ。

学習した重みを保存する。転移学習に使えるよう各レイヤーに名前わつけて、(name = ‘PC1’など)combinedではなけgenerateorの重みを保存する。

品詞の列を生成するニューラルネットをpos_makerと名付ける

    def build_pos_maker(self):
        model = Sequential()
    
        model.add(Dense(256 * int(self.img_rows * self.img_cols), activation="relu",
                          input_dim=self.latent_dim, name = 'PD1'))
        model.add(Reshape((self.img_cols , self.img_rows, 256)))
        model.add(UpSampling2D())
        model.add(Conv2D(128, kernel_size=3, strides=2, padding="same", name = 'PC1'))
        model.add(BatchNormalization(momentum=0.8, name = 'PB1'))
        model.add(Activation("relu"))
        model.add(UpSampling2D())
        model.add(Conv2D(128, kernel_size=3, strides=2, padding="same", name = 'PC2'))
        model.add(BatchNormalization(momentum=0.8, name = 'PB2'))
        model.add(Activation("relu"))
        model.add(UpSampling2D())
        model.add(Conv2D(64, kernel_size=3, strides=2, padding="same", name = 'PC3'))
        model.add(BatchNormalization(momentum=0.8, name = 'PB3'))
        model.add(Activation("relu"))
        model.add(UpSampling2D())
        model.add(Conv2D(32, kernel_size=3, strides=2, padding="same", name = 'PC4'))
        model.add(BatchNormalization(momentum=0.8, name = 'PB4'))
        model.add(Activation("relu"))
        model.add(Conv2D(1, kernel_size=3, strides=1, padding="same", name = 'PC5'))
        model.add(Activation("tanh"))
    
        model.summary()
        
        noise = Input(shape=(self.latent_dim,))
        print(noise)
        pos = model(noise)
    
        return Model(noise, Container(noise, pos)(noise), name='pos_gen')

//略

        self.generator.save_weights('pos_gan_weight.h5')

2つの学習機をつなげる

        # The generator takes noise as input and generates imgs
        z = Input(shape=(self.latent_dim,))
        pos = self.pos_maker(z)
        img = Input(shape=self.txt_shape)
        
        fake = self.generator(pos)
        # For the combined model we will only train the generator
        #Fix pos_maker weights
        self.pos_maker.load_weights('pos_gan_weight.h5', True)
        self.pos_maker.trainable = False

z(雑音)をpos_makerで品詞の列に変換して、generatorで単語にする。pos_makerの重みは更新しない。

2つめの学習機を初期化する

入力に品詞出力に単語のW2V列をセットして学習するこれを初期値とする。

    def train_pos_to_word(self):
        pos_to_w2v = self.generator
        pos_to_w2v.compile(loss='mse',
        optimizer='adam')
        X_train = joblib.load('{0}/pos_labels.pkl'.format(WRITE_JOBLIB_DIR))
        X_train = np.reshape(X_train, (-1,self.img_cols, self.img_rows, 1))
        X_test = joblib.load('{0}/vectorized_words.pkl'.format(WRITE_JOBLIB_DIR))
        X_test = np.reshape(X_test, (-1, self.img_cols, self.img_rows , self.channels))
        pos_to_w2v.fit(X_train, X_test, batch_size=32,
            epochs=12,
            verbose=1)

結果

かかった時間は手元のパソコンとGPUサーバーで2倍ほどの差があった。

Epoch5

Epoch40

Epoch400

長く学習するとモードがなくなって悪化する。このほうほうではすく偏るらしく元のサイトでも課題になっていた。

Epoch40のときの生成分がこちら。

収録オニヅカ原典する通りは単体。

がけっぷち相当し初めてリリース。。。

詳細は記述実際は彼奴。。

すべてリリース必ずた。。。。

巻が第発売全リリース。。

は本来の年リリース。。。

はエピローグ相当する両性それぞれ取消。

が」全巻。。。。

巻から日発売。。。。

スケリグカン・シヌ。。。。。。

第初めてリリースは取消。。。

ミッションイー。。。。。。。

元データが会話文ではないので名詞が多い。

またこの手のやつは大体そうだが、別に意味を理解しているわけではないので、意味不明な文も結構生成されている。

まとめ

DCGANで文章生成は品詞の列は作れることはわかった。

ただ文章生成になると微妙なので、別の手法(別のGANとかRNN系)も試してみて比較したほうが良さそう。