機械学習であれな文章を使って単語を学習させ、短文を作らせるようにした話 のやったことの詳細です
ただし、この方法については既存のフレームワークは使用しておらず
書籍:"ゼロから作るDeep Learning❷"で提供されているライブラリをベースにしているため、
前提としてここのライブラリが必要になります
(一応githubでソースは公開されていますが、おそらく購入特典かと思いますので、ここでは掲載しません。。)
また、ここで示した方法は簡易的であるため精度についてはあまりよくないです。
本格的な自然言語処理については別の方法をご確認ください
大雑把な流れは下記になります。
- 環境整備
- ノクターンノベルから年間Top3の作品をスクレイピング
- スクレイピングした文章を単語ごとに分割する"わかち"
- 機械学習を用いて文章を学習させる。
- 学習させたたモデリングを使って、入力した単語から短文を作成する
前提
環境はローカルで全部実行する方法が一番シンプルかと思いますが、
我が家の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を作成します
下記は一例です
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に対して以下の感じで記載します。パスは各自の構成に合わせてください
最後に、ランタイムをGPUに変更します。 下記画像のように設定します
実行
いよいよ実行です google colabを上から順に実行すれば、学習が開始されます ただし、これでも2,3時間かかるので気長に待ちましょう。
文章作成
前回の学習が終わると、BetterRnnlm.pkl
が作成されます。これを使って文章作成をします。
ここまでくると、もうすぐです。
google colab上で実行するとBetterRnnlm.pkl
がgoogle 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