機械学習から短文を作らせた話(実装編)

機械学習であれな文章を使って単語を学習させ、短文を作らせるようにした話 のやったことの詳細です

ただし、この方法については既存のフレームワークは使用しておらず
書籍:"ゼロから作るDeep Learning❷"で提供されているライブラリをベースにしているため、
前提としてここのライブラリが必要になります

www.oreilly.co.jp

(一応githubでソースは公開されていますが、おそらく購入特典かと思いますので、ここでは掲載しません。。)

また、ここで示した方法は簡易的であるため精度についてはあまりよくないです。
本格的な自然言語処理については別の方法をご確認ください

大雑把な流れは下記になります。

  1. 環境整備
  2. ノクターンノベルから年間Top3の作品をスクレイピング
  3. スクレイピングした文章を単語ごとに分割する"わかち"
  4. 機械学習を用いて文章を学習させる。
  5. 学習させたたモデリングを使って、入力した単語から短文を作成する

前提

環境はローカルで全部実行する方法が一番シンプルかと思いますが、
我が家のMacbook Air M1のスペックでも、進行具合を見るに学習時に2,3日かかることが予想されました。
ここではすでにある程度環境整備が済んでいるGoogle Colabと、
ローカルを併用して実行することとします

0.環境整備

ローカル編

ここではすでに"ゼロから作るDeep Learning❷"のライブラリをローカルに展開し
必要なライブラリをインストールしていることを前提として話を進めます。
使用するフォルダ及びファイルは下記になります

(root)
|-common
|-dataset
   |- ptb.py
|-ch06
   |- better_rnnlm.py
   |- rnnlm_gen.py
   |- train_better_rnnlm.py
|-ch07
   |- generate_better_text.py

Google Colab編

必須ではないです。ローカルでも全て実行可能です。

ファイルは2つ作ります。
スクレイピング用のcolabについては、適当なgoogle driveに新たにフォルダを作成し、そこをベースにgoogle colaboratoryを作成します
下記は一例です

f:id:onthebacksoftheflyer:20210313142623p:plain
スクレイピング実行一例

1.スクレイピング

まずノクターンノベルから作品をスクレイピングします。
自分はここについてはGoogle Colabで実行しております

google colaboratoryを新規作成します。
ブラウザ上でPythonコードを入力できる状態にします
ライブラリをインストールするために、下記をインストールします

!pip install readability-lxml
!pip install html2text
!pip install janome

次にライブラリをインポートします。

import requests
import urllib
from bs4 import BeautifulSoup
from readability.readability import Document
import html2text
import re

次にスクレイピングする作品のURL及び、スクレイピングはtxtファイルで保存されるので、そのtxtファイルの名前を設定します

# 作品URL
urls = ['https://novel18.syosetu.com/n4913gc/']
# 出力ファイル名
filename = 'nocturn_kankin.txt'

ここから本題のスクレイピングです。
ノクターンノベルの仕様上、ユーザーエージェントとcookieにてover18かどうかの設定が必要になります
注)結構時間かかります(20分前後)

text_list = []

def extract_body(url):
    #set UA
    header = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:62.0) Gecko/20100101 Firefox/62.0"}
   
    #set cookie
    cookie = {"over18": "yes"}

    page = 1
    is_articles = True
    body_list = []
    while is_articles:
      # URLを設定し接続     
      chapterlistUrl = "{}{}/".format(url,page)
      responseCL  = requests.get(url=chapterlistUrl, headers=header, cookies=cookie)
      chapterlistHtml = responseCL.content
      if page == 1 or page % 10 == 0:
        print('page_{0}:実行中'.format(page))
        print(chapterlistUrl)
      # ページごとに本文を抽出
      soup = BeautifulSoup(chapterlistHtml, "html.parser")
      bodys = soup.find_all("div", id="novel_honbun")
      if len(bodys) != 0:
        for body in bodys: 
          body_list.append(body.text)
        page += 1
      else:
        is_articles = False
    body_text = ' '.join(body_list)
    return body_text

for url in urls:
    print(url)
    text_list.append(extract_body(url))

これが終わると、作品の小説がリストとして出来上がります
あとはそれをtxtにしてダウンロードします

with open(filename, 'w', encoding='utf-8') as f:
   for x in text_list:
       f.write(str(x) + ";;\n")
print("file is saved")

生成したtxtファイルをダウンロードします

from google.colab import files
files.download(filename)

続きに行く前に、このモデルは学習用作品、テスト用作品、パラメータ調整検証用作品の三つが必要ですので
3作品以上スクレイピングが必要になります。
一番いいのは1作品を学習用、テスト用、検証用の三分割にする方がいいと思いますが、ここでは簡易的に3作品をスクレイピングする形とします。

2.わかち

これで作品をtxtファイルにできました。ただし、現時点では文章は単語同士で繋がっており、このままだと機械学習にかけても大したものはできません。
文章を単語に分ける必要があります。これを"わかち"と呼ぶようです
ここではMecabを使ってわかちを実行します。こういうツール開発チームには頭が上がりません taku910.github.io


まずMeCabをインストールします。ここではmacを想定します

$ brew install mecab
$ brew install mecab-ipadic
$ pip install mecab-python3

続いて、MeCabの性能を引き上げるために、neologd辞書をインストールします

$ brew install mecab mecab-ipadic git curl xz # 必要なもののみインストールする
$ git clone --depth 1 git@github.com:neologd/mecab-ipadic-neologd.git
cd mecab-ipadic-neologd
./bin/install-mecab-ipadic-neologd -n


ここでは新たにフォルダー(例:wakachi)を作りその中で作業をします。
この中に先ほどダウンロードファイルと、新たに一つファイルを作成します(wakachi.py)

(root)
|-wakachi
   |- wakachi.py
   |- nocturn_kankin.txt

そしてwakachi.pyは下記のようにします

###使用方法
# コマンド実行
#$ python wakachi.py nocturn_kankin.txt
###
# mecab 大文字小文字に注意
import MeCab
# datetime
import time
# 引数取得
import sys
from sys import argv
import os

def mecab_parse(data):
    # 分かち書きのみ出力する設定にする
    mecab = MeCab.Tagger('-Owakati -d /usr/local/lib/mecab/dic/mecab-ipadic-neologd -b 16384')
    text = mecab.parse(data)
    
    return text

def splitStr2(str, num):
    l = []
    for i in range(num):
        l.append(str[i::num])
    l = ["".join(i) for i in zip(*l)]
    rem = len(str) % num  # zip で捨てられた余り
    if rem:
        l.append(str[-rem:])
    return l

# mecabの読み込み限界文字列数
limit = 2600000
print('実行中…')
# 引数の取得
input_file_name= sys.argv[1]

# 解析対象テキストファイルを開く
f =  open(input_file_name,'r')
# ファイルを読み込む
data = f.read()

text = ''
if len(data) > limit:
    mecab_list = []
    strLists = splitStr2(data, limit)
    for data_str in strLists:
        text = data_str + '\n'
    text = mecab_parse(data)

else:
    # 分かち書きのみ出力する設定にする
    text = mecab_parse(data)

# ファイル
# 同一ディレクトリに出力
replace_input_file_name = input_file_name.replace('.txt', '')
out_file_name = replace_input_file_name + "_wakachi.txt"
try:
    os.makedirs(replace_input_file_name)
    with open(os.path.join(replace_input_file_name, out_file_name), 'w') as f:
        f.write(text)
except FileExistsError:
    with open(out_file_name, 'w') as f:
        f.write(text)
print('ファイル出力完了 ファイル名:'+ out_file_name)

そして、コマンドラインからこのファイルがあるディレクトリへ移動し、下記を実行します

$ python wakachi.py nocturn_kankin.txt

これでわかちができます

文字数が多い場合

ただし、文字数があまりにも多いとMecabが対応できません。その際は小説のファイルを分割します

下記ファイルを新規作成します。

###使用方法
# コマンド実行
#$ python file_split.py minimum.txt
###

# 引数取得
import sys
from sys import argv

def splitStr2(str, num):
    l = []
    for i in range(num):
        l.append(str[i::num])
    l = ["".join(i) for i in zip(*l)]
    rem = len(str) % num  # zip で捨てられた余り
    if rem:
        l.append(str[-rem:])
    return l

# mecabの読み込み限界文字列数
limit = 2000000

print('実行中…')
# 引数の取得
input_file_name= sys.argv[1]
# 解析対象テキストファイルを開く
f =  open(input_file_name,'r')
# ファイルを読み込む
data = f.read()

if len(data) > limit:
    mecab_list = []
    strLists = splitStr2(data, limit)
    for index, data_str in enumerate(strLists):
        #出力ファイル名
        out_file_name = input_file_name.replace('.txt', '') + "_" + str(index) + ".txt"
        with open(out_file_name, 'w') as f:
            f.write(data_str)
        print('ファイル出力完了 ファイル名:'+ out_file_name)

そして下記コマンドを実行すると、ファイル名のフォルダが作成され、その中に分割したファイルが出力されます

$ python file_split.py nocturn_kankin.txt

これでわかちができました。

機械学習を用いて文章を学習させる。

いよいよ本題です。
わかちファイルを用いて実際にモデルを作成します

データセットの整備

まず、datasetについて設定します。
ここでは先ほどのtxtファイルとptb.pyファイルを設定します

(root)
|-common
|-dataset
   |- ptb.py
   |- nocturn_kankin_wakachi.txt (例: 学習用)
   |- nocturn_tensei_slave_wakachi.txt(例: テスト用)
   |- nocturn_tundele_wakachi.txt(例: 検証用)

次にptb.pyの整備を行います。中身は教材ではgithubからtxtをダウンロードする形ですが
今回はローカルにtxtがあるのが前提ですので、それに合わせます

# coding: utf-8
import sys
import os
sys.path.append('..')
try:
    import urllib.request
except ImportError:
    raise ImportError('Use Python3!')
import pickle
import numpy as np


key_file = {
    'train':'nocturn_kankin_wakachi.txt', #学習用ファイル(ファイル名に合わせてください)
    'test':'nocturn_tensei_slave_wakachi.txt', #テスト用ファイル(ファイル名に合わせてください)
    'valid':'nocturn_tundele_wakachi.txt' #検証用ファイル(ファイル名に合わせてください)
}
save_file = {
    'train':'nocturn.train.npy',
    'test':'nocturn.test.npy',
    'valid':'nocturn.valid.npy'
}
vocab_file = 'nocturn.vocab.pkl'

dataset_dir = os.path.dirname(os.path.abspath(__file__))


def load_vocab():
    vocab_path = dataset_dir + '/' + vocab_file

    if os.path.exists(vocab_path):
        with open(vocab_path, 'rb') as f:
            word_to_id, id_to_word = pickle.load(f)
        return word_to_id, id_to_word

    word_to_id = {}
    id_to_word = {}
    data_type = 'train'
    file_name = key_file[data_type]
    file_path = dataset_dir + '/' + file_name

    words = open(file_path).read().replace('\n', '<eos>').strip().split()

    for i, word in enumerate(words):
        if word not in word_to_id:
            tmp_id = len(word_to_id)
            word_to_id[word] = tmp_id
            id_to_word[tmp_id] = word

    with open(vocab_path, 'wb') as f:
        pickle.dump((word_to_id, id_to_word), f)

    return word_to_id, id_to_word


def load_data(data_type='train'):
    '''
        :param data_type: データの種類:'train' or 'test' or 'valid (val)'
        :return:
    '''
    if data_type == 'val': data_type = 'valid'
    save_path = dataset_dir + '/' + save_file[data_type]

    word_to_id, id_to_word = load_vocab()

    if os.path.exists(save_path):
        corpus = np.load(save_path)
        return corpus, word_to_id, id_to_word

    file_name = key_file[data_type]
    file_path = dataset_dir + '/' + file_name

    words = open(file_path).read().replace('\n', '<eos>').strip().split()

    # corpus = np.array([word_to_id[w] for w in words])
    # 教材のソースをそのまま利用するとここでエラーが出るので、簡易的に細工
    if data_type == 'train':
        corpus = np.array([word_to_id[w] for w in words])
    else:
        corpus = np.array([word_to_id[w] for w in words if w in word_to_id])

    np.save(save_path, corpus)
    return corpus, word_to_id, id_to_word


if __name__ == '__main__':
    for data_type in ('train', 'val', 'test'):
        load_data(data_type)

 学習モデル

続いて学習モデルの生成です。 フォルダ構成については好みですが、ここでは新たにフォルダを切って、この中で作業をします。フォルダにはこれらファイルを格納します

(root)
|-common
   |-(教材の通り)
|-ch06
   |- rnnlm.py
   |- better_rnnlm.py
   |- rnnlm_gen.py
   |- train_better_rnnlm.py
|-ch07
   |- generate_better_text.py

から一部ファイル(rnnlm.py, better_rnnlm.py)をcommonにうつします

(root)
|-common
   |-(教材の通り)
   |- rnnlm.py
   |- better_rnnlm.py
|-dataset
   |- ptb.py
   |- nocturn_kankin_wakachi.txt (例: 学習用)
   |- nocturn_tensei_slave_wakachi.txt(例: テスト用)
   |- nocturn_tundele_wakachi.txt(例: 検証用)
|-ch06
   |- rnnlm_gen.py
   |- train_better_rnnlm.py
|-ch07
   |- generate_better_text.py

次に、その他学習ファイル、及び文章生成に必要なファイルを全て新たにフォルダを切ってまとめてしまいます
最終的なフォルダ構成は下記になります。

(root)
|-common
   |-(教材の通り)
   |- rnnlm.py
   |- better_rnnlm.py
|-dataset
   |- ptb.py
   |- nocturn_kankin_wakachi.txt (例: 学習用)
   |- nocturn_tensei_slave_wakachi.txt(例: テスト用)
   |- nocturn_tundele_wakachi.txt(例: 検証用)
|-learn
  |-generate_better_text.py
  |-train_better_rnnlm.py

次に、learn.train_better_rnnlm.pyを編集します。 具体的には教材ソースをベースに、ライブラリを一部移動し、datasetも変更したので、それを合わせます 以下は一例です

# coding: utf-8
import sys
sys.path.append('..')
from common import config
# GPUで実行する場合は下記のコメントアウトを消去(要cupy)
# ==============================================
# config.GPU = True
# ==============================================
from common.optimizer import SGD
from common.trainer import RnnlmTrainer
from common.util import eval_perplexity, to_gpu
from dataset import ptb  # ★ 今回の入力データに合わせる
from common.better_rnnlm import BetterRnnlm # 移動したbetter_rnnlmにパスを合わせる


# ハイパーパラメータの設定
batch_size = 20
wordvec_size = 650
hidden_size = 650
time_size = 35
lr = 20.0
max_epoch = 40
max_grad = 0.25
dropout = 0.5

# 学習データの読み込み
corpus, word_to_id, id_to_word = ptb.load_data('train')  # ★コーパス変更
corpus_val, _, _ = ptb.load_data('val')  # ★コーパス変更
corpus_test, _, _ = ptb.load_data('test')  # ★コーパス変更

print(len(corpus))
print(len(corpus_val))
print(len(corpus_test))


if config.GPU:
    corpus = to_gpu(corpus)
    corpus_val = to_gpu(corpus_val)
    corpus_test = to_gpu(corpus_test)

vocab_size = len(word_to_id)
xs = corpus[:-1]
ts = corpus[1:]

model = BetterRnnlm(vocab_size, wordvec_size, hidden_size, dropout)
optimizer = SGD(lr)
trainer = RnnlmTrainer(model, optimizer)

best_ppl = float('inf')
for epoch in range(max_epoch):
    trainer.fit(xs, ts, max_epoch=1, batch_size=batch_size,
                time_size=time_size, max_grad=max_grad)

    model.reset_state()
    ppl = eval_perplexity(model, corpus_val)
    print('valid perplexity: ', ppl)

    if best_ppl > ppl:
        best_ppl = ppl
        model.save_params()
    else:
        lr /= 4.0
        optimizer.lr = lr

    model.reset_state()
    print('-' * 50)


# テストデータでの評価
model.reset_state()
ppl_test = eval_perplexity(model, corpus_test)
print('test perplexity: ', ppl_test)

Google colabで実行

これで準備が整いました。
ここから実行するわけですが、如何せん実行しても相当重いです。
我が家のMacbook Air M1でも3日ほどかかりました。
そこで、Google colab上で実行することにします。

準備として、train_better_rnnlm.pyのヘッダの一部を変更します。config.GPUをtrueにします

# GPUで実行する場合は下記のコメントアウトを消去(要cupy)
# ==============================================
config.GPU = True
# ==============================================


次に、これら下記のフォルダ構成をgoole driveの適当なところにアップします。

(root)
|-common
   |-(教材の通り)
   |- rnnlm.py
   |- better_rnnlm.py
|-dataset
   |- ptb.py
   |- nocturn_kankin_wakachi.txt (例: 学習用)
   |- nocturn_tensei_slave_wakachi.txt(例: テスト用)
   |- nocturn_tundele_wakachi.txt(例: 検証用)
|-learn
  |-generate_better_text.py
  |-train_better_rnnlm.py

もし、一回実行したりしてpycacheフォルダがある場合は削除してください。

なおpycacheフォルダーはコンパイル済みのモジュールがキャッシュされる場所なのでアップロードはしないでください。Google Colabで実行すると自動的に生成されます

次に、learnフォルダに対して、google colaboratoryファイルを作成します 画像はいろいろ違っていますが一例です。

一例
一例

次にgoogle colaboratoryに対して以下の感じで記載します。パスは各自の構成に合わせてください google colabの一例

最後に、ランタイムをGPUに変更します。 下記画像のように設定します ランタイム変更方法1 ランタイム変更2

 実行

いよいよ実行です google colabを上から順に実行すれば、学習が開始されます ただし、これでも2,3時間かかるので気長に待ちましょう。

文章作成

前回の学習が終わると、BetterRnnlm.pklが作成されます。これを使って文章作成をします。
ここまでくると、もうすぐです。
google colab上で実行するとBetterRnnlm.pklgoogle driveに保存されるので、
まずはこれをローカルにダウンロードし(ファイルを右クリックからダウンロード可能)
learnフォルダに配置します

(root)
|-learn
  |-BetterRnnlm.pkl
  |-generate_better_text.py
  |-train_better_rnnlm.py

次にこれも好みですがcommonフォルダにch06フォルダにあるrnnlm_gen.pyを移動します

(root)
|-common
  |-rnnlm_gen.py
  |-その他

rnnlm_gen.pyのヘッダの一部を変更します

# coding: utf-8
import sys
sys.path.append('..')
import numpy as np
from common.functions import softmax
from common.rnnlm import Rnnlm #ch06.better_rnnlmから変更
from common.better_rnnlm import BetterRnnlm #ch06.better_rnnlmから変更

次にlearn.generate_better_text.pyの一部編集します

###使用方法
# コマンド実行
#$ python generate_better_text.py 入力文字列
###
# coding: utf-8
import sys
sys.path.append('..')
from common.np import *
from common.rnnlm_gen import BetterRnnlmGen #ch06から変更
from dataset import ptb

・・・
model = BetterRnnlmGen(vocab_size=vocab_size)
model.load_params('BetterRnnlm.pkl')

# python generate_better_text.py 入力文字列

# 今回のデータセットに合わせて変更。日本語に合わせる
# start文字とskip文字の設定
start_word = sys.argv[1]
start_id = word_to_id[start_word]
skip_words = []  
skip_ids = [word_to_id[w] for w in skip_words]
# 文章生成
word_ids = model.generate(start_id, skip_ids, sample_size=120)
eos_id = word_to_id['<eos>']
txt = ''.join([id_to_word[i] if i != eos_id else '。\n' for i in word_ids])
txt = txt.replace('\n\n', '\n')  # 空行の除去
txt = txt.replace('」。\n', '」\n')  # 会話の最後に句点をつけてしまったものを除去
print(txt)

ここまで来たら実行可能になります

実行

コマンドラインから実行します。
コマンドラインでgenerate_better_text.pyのあるフォルダに遷移し、下記を実行します

$ python generate_better_text.py 入力文字列

すると短文が出力されます。 以下一例です らめぇ

短文文字数調整

もし、出力文字数を変更したい場合は、generate_better_text.py のmodel.generateしているところのsample_sizeを調整します

# 文章生成
word_ids = model.generate(start_id, skip_ids, sample_size=120) ### sample_sizeの数字を上下させることで文字数調整

お疲れ様でした。

参考

今回の学習モデル作成にあたり、大いに参考になりました書籍、記事です。ありがとうございます。

名著です www.oreilly.co.jp

note.com

qiita.com

qiita.com