自動作文の作成について

hanamichi-sukusuku.hatenablog.com

こちらで紹介したマルコフ連鎖を用いた自動作文ではオウム返しのようなやりとりになってしまったり、辞書からランダムに単語を取得していることからあまり実用的でないプログラムになっている。しかし、応用できる箇所はいろいろあり、例えば応答文を作成する場合、上記のリンクでは単語の品詞に注目し、始点となる単語を決めてしまっているが、これを機械学習を用いて文章の意図を判定し、その結果を始点に応答文を作成するなら、今回のようなオウム返しのような返答ではなく、もう少し実際の会話に近い応答を返すことが可能だと思われる。

さらに、辞書を充実させたり、会話の規則を作ったりすることもできる。マルコフ連鎖以外にも目を向けていくと、ディープラーニングを用いたLSTM(Long Short Term-Memory)やRNN(Recurrent Neural Network)も文章を自動生成する手法として有名なため、それらの手法を利用して自動作文に挑戦してみることもできる。

 

オープンなテキストソース

自然言語処理にはある程度規模を持ったテキストリソースが必須になる。自身で用意するのは大変なのでオープンなテキストリソースの紹介をしていく。

 

wikipedia

世界最大のフリー百科事典。

 

https://ja.wikipedia.org/

 

青空文庫

著作権の消滅した作品では知的財産権が発生しないのでかなり自由に利用できる。

ただ、非常に古い作品になるので使い道によっては問題があるかも。

 

http://www.aozora.gr.jp/

 

livedoor ニュースコーパス

2012年までのデータとなっているが提供媒体ごとのニュース記事がしっかりと分類されている。文字コードUTF-8で統一されたテキストファイルで提供されているので、プログラムからでも扱いやすいデータになっている。

 

http://www.rondhuit.com/download.html#ldoc

 

 

 

 

 

 

 

マルコフ連鎖を用いて自動作文の作成

今回のプログラムではマルコフ連鎖を用いて、自動作文を作成していく。

マルコフ連鎖とは未来の状態が現在の状態によって決まるという確率過程のことで今回のプログラムに当てはめると、辞書型のオブジェクトに形態素解析した単語を保存していき、それを辞書として入力された文字から既存の文章を利用し、自動で文章を作成するというような形で現在の状態から未来を推測していくのがマルコフ連鎖である。

 

流れとしては

・辞書の作成(辞書型のオブジェクトに形態素解析した要素を追加していく)

・始点となる単語と辞書を使って作文する。

 

今回作成する辞書は前後の結びつきに注目して、3単語ずつ登録していく。

 

import MeCab

import os,json,random

 

dict_file = "markov_dict.json"

dic = {}

 

# 辞書への登録 --- (*1)

def regist_dic(wordlist):

    global dic

    w1 = ""

    w2 = ""

    

    # 要素が3未満の場合は、何もしない

    if len(wordlist) < 3 : return

    

    for w in wordlist :

        word = w[0]

        if word == "" or  word == "\r\n" or word == "\n" : continue

        # 辞書に単語を設定

        if w1 and w2 :

            set_dic(dic,w1, w2, word)

        # 文末を表す語のの場合、連鎖をクリアする

        if word == "。" or word == "?" or  word == "?" :

            w1 = ""

            w2 = ""

            continue

        # 次の前後関係を登録するために、単語をスライド

        w1, w2 = w2, word

    

    # 辞書を保存

    json.dump(dic, open(dict_file,"w", encoding="utf-8"))

 

# 辞書に単語を設定 --- (*2)

def set_dic(dic, w1, w2, w3):

    # 新しい単語の場合は、新しい辞書オブジェクトを作成

    if w1 not in dic : dic[w1] = {}

    if w2 not in dic[w1] : dic[w1][w2] = {}

    if w3 not in dic[w1][w2]: dic[w1][w2][w3] = 0

    # 単語の出現数をインクリメントする

    dic[w1][w2][w3] += 1

 

# 応答文の作成 --- (*3)

def make_response(word):

    res =

    

    # 「名詞」/「形容詞」/「動詞」は、文章の意図を示していることが多いと想定し、始点の単語とする。

    w1 = word

    res.append(w1)

    w2 = word_choice(dic[w1])

    res.append(w2)

    while True:

        # w1,w2の組み合わせから予想される、単語を選択

        if w1 in dic and w2 in dic[w1] : w3 = word_choice(dic[w1][w2])

        else : w3 = ""

        res.append(w3)

        # 文末を表す語の場合、作文を終了

        if w3 == "。" or w3 == "?" or  w3 == "?"  or w3 == "" :  break

        # 次の単語を選択するために、単語をスライド

        w1, w2 = w2, w3

    return "".join(res)

        

def word_choice(candidate):

    keys = candidate.keys()

    return random.choice(list(keys))

 

# メイン処理 --- (*4)

 

# 辞書がすでに存在する場合は、最初に読み込む

if os.path.exists(dict_file):

        dic = json.load(open(dict_file,"r"))

        

while True:

    # 標準入力から入力を受け付け、「さようなら」が入力されるまで続ける

    text = input("You -> ")

    if text == "" or text == "さようなら" :

        print("Bot -> さようなら")

        break

 

    # 文章整形

    if text[-1] != "。" and text[-1] != "?" and text[-1] != "?" : text +="。"

    

    # 形態素解析

    tagger = MeCab.Tagger("-d /var/lib/mecab/dic/mecab-ipadic-neologd")

    tagger.parse("")

    node =  tagger.parseToNode(text)

    

    # 形態素解析の結果から、単語と品詞情報を抽出

    wordlist =

    while node is not None:

        hinshi = node.feature.split(",")[0]

        if  hinshi not  in ["BOS/EOS"]:

            wordlist.append([node.surface,hinshi])

        node = node.next

    

    # マルコフ連鎖の辞書に登録

    regist_dic(wordlist)

 

    # 応答文の作成

    for w in wordlist:

        word = w[0]

        hinshi = w[1]

        # 品詞が「感動詞」の場合は、単語をそのまま返す

        if hinshi in [ "感動詞"] :

            print("Bot -> " + word)

            break

        # 品詞が「名詞」「形容詞」「動詞」の場合で、かつ、辞書に単語が存在する場合は、作文して返す

        elif (hinshi in [ "名詞" ,"形容詞","動詞"]) and (word in dic):

            print("Bot -> " + make_response(word))

            break

 

実行結果

f:id:hanamichi_sukusuku:20210125210935p:plain

You->が入力したもの

Bot->が自動作成された文章

猫や犬という単語を始点として辞書を元に文章を作成している。

「猫はどうですか?」と「猫は可愛い」と出力内容が違うのは動詩、形容詞からランダムに出力しているからである。このプログラムでは単語の意味や構文を考えて出力しているわけではないので作文の質を高めるには様々な改善を行う必要がある。それは前提においてプログラムを見ていく。

 

モジュールインポート

import MeCab
import os,json,random

 

jsonモジュールでは作成した辞書を外部に保存することで内容を更新するためにjson形式で辞書を保存するためや読み込むためにインポートする。

randomモジュールではランダムに値を取得するためにインポート。

 

辞書の保存名と辞書型のオブジェクトを定義

dict_file = "markov_dict.json"
dic = {}

 

 

辞書が既に存在している場合には、最初に辞書を読み込む

# 辞書がすでに存在する場合は、最初に読み込む
if os.path.exists(dict_file):
 dic = json.load(open(dict_file,"r"))

 

os.path.exists()で指定したファイル名が存在するかどうかの判定。

上記が存在すればjson.load(open(dict_file, "r"))でファイルを読み取り用で開き、json.loadで読み込む。

 

ループ処理内で関数を呼び出し辞書の作成や辞書を元に作成した文章を出力

while True:
  # 標準入力から入力を受け付け、「さようなら」が入力されるまで続ける 
   text = input("You -> ")
   if text == "" or text == "さようなら" :
    print("Bot -> さようなら")
    break

  # 文章整形
   if text[-1] != "。" and text[-1] != "?" and text[-1] != "?" : text +="。"
 
   tagger = MeCab.Tagger("-d /var/lib/mecab/dic/mecab-ipadic-neologd")
   tagger.parse("")
   node = tagger.parseToNode(text)
 
   # 形態素解析の結果から、単語と品詞情報を抽出
   wordlist =
   while node is not None:
    hinshi = node.feature.split(",")[0]
    if hinshi not in ["BOS/EOS"]:
     wordlist.append([node.surface,hinshi])
   node = node.next
 
  # マルコフ連鎖の辞書に登録
  regist_dic(wordlist)

   # 応答文の作成
   for w in wordlist:
     word = w[0]
     hinshi = w[1]
   # 品詞が「感動詞」の場合は、単語をそのまま返す
     if hinshi in [ "感動詞"] :
       print("Bot -> " + word)
       break
   # 品詞が「名詞」「形容詞」「動詞」の場合で、かつ、辞書に単語が存在する場合は、作文して返す
     elif (hinshi in [ "名詞" ,"形容詞","動詞"]) and (word in dic):
       print("Bot -> " + make_response(word))
       break

 

input()で入力を受け取り、さようならと入力された時処理を終了する。

if text[-1] != "。" and text[-1] != "?" and text[-1] != "?" : text +="。"

ここでは入力された最後の文字が句読点やクエスチョンマークがない時、入力されたものに句読点を付与している。

tagger = MeCab.Tagger("-d /var/lib/mecab/dic/mecab-ipadic-neologd")
tagger.parse("")
node = tagger.parseToNode(text)

ここで形態素解析を行う。

wordlist =
while node is not None:
  hinshi = node.feature.split(",")[0]
  if hinshi not in ["BOS/EOS"]:
   wordlist.append([node.surface,hinshi])
  node = node.next

 

変数hinshiに["BOS/EOS"]が含まれない時、リストに形態素解析した単語の表層形と品詞情報を追加。

 

regist_dic(wordlist)

マルコフ連鎖の辞書に登録するため単語と品詞情報を持ったリストをregist_dic()関数に渡す。

 

regist_dic()関数、辞書への登録、保存

def regist_dic(wordlist):
 global dic
 w1 = ""
 w2 = ""
 
 # 要素が3未満の場合は、何もしない
 if len(wordlist) < 3 : return
 
 for w in wordlist :
  word = w[0]
  if word == "" or word == "\r\n" or word == "\n" : continue
  # 辞書に単語を設定
  if w1 and w2 :
   set_dic(dic,w1, w2, word)
  # 文末を表す語のの場合、連鎖をクリアする
  if word == "。" or word == "?" or word == "?" :
   w1 = ""
   w2 = ""
   continue
  # 次の前後関係を登録するために、単語をスライド
  w1, w2 = w2, word
 
 # 辞書を保存
 json.dump(dic, open(dict_file,"w", encoding="utf-8"))

 

global 変数名とすることで関数内でグローバル変数として扱われるようになり、関数外で定義した変数の中身も更新されるようになる。

 

この関数の引数として受け取っているwordlistは入力された文章を形態素解析し、それぞれの単語と品詞が格納されているリストである。

if w1 and w2:の部分はif文の条件式では変数のみを指定すると文字列型なら空じゃなければTrueを返すことを利用している。そして、w1、w2どちらにも文字列が格納されている時、set_dic()関数を呼び出し、辞書に単語を設定していく。

 

w1, w2 = w2, wordでは一つの単語に対して前後3つまでの関係を登録するため一文字ずつずらし、for文とset_dic()関数で繰り返し辞書に登録していく。

最後にjson.dump()で作成した辞書を外部に保存する。外部に保存することで辞書の内容を更新し続けることができる。第一引数に保存する辞書を指定、第二引数で保存先(openでファイルを書き込み用で開きutf-8を指定している)

 

set_dic()関数、辞書に単語を設定

def set_dic(dic, w1, w2, w3):
  # 新しい単語の場合は、新しい辞書オブジェクトを作成
  if w1 not in dic : dic[w1] = {}
  if w2 not in dic[w1] : dic[w1][w2] = {}
  if w3 not in dic[w1][w2]: dic[w1][w2][w3] = 0
  # 単語の出現数をインクリメントする
  dic[w1][w2][w3] += 1

dicは辞書型のオブジェクトで今回自動作文に使用する辞書。

w1は入力された先頭の単語から始まり一つずつ後の単語にずれていく。w2は二番目に位置する単語から一つずつ後にずれていく。w3はregist_dic関数内のfor文で現在取得している単語。

入力 猫は可愛い。

w1 = ""

w2 = ""

regist_dic()関数内では初めこのように定義されており文字が格納されていないとif文で弾かれるのでset_dic()関数の引数は

dic, w1="猫", w2="は", w3="可愛い"このようになる。

 

次の処理では

dic, w1="は",w2="可愛い",w3="。"のようにずれていき、「。」や「?」などの文末を表す文字が出てきたら処理を終了する。

このような辞書が出来上がる。

f:id:hanamichi_sukusuku:20210125221605p:plain

一部をピックアップしてみると

'猫': {'は': {'可愛い': 5, 'どう': 3}}

'どう': {'です': {'か' : 7}}

今回のプログラムではこの辞書を元に自動作文されるのでこの辞書を作成した場合、猫と入力すると猫を始点として文章を作成するので「猫+は+(可愛いorどう)+です+か」という文章が出来上がる。可愛いorどうの部分はランダム。

 

 

while文に戻り、regist_dic()関数の次の処理へ

応答文の作成

for w in wordlist:
 word = w[0]
 hinshi = w[1]
 # 品詞が「感動詞」の場合は、単語をそのまま返す
 if hinshi in [ "感動詞"] :
  print("Bot -> " + word)
  break
 # 品詞が「名詞」「形容詞」「動詞」の場合で、かつ、辞書に単語が存在する場合は、作文して返す
 elif (hinshi in [ "名詞" ,"形容詞","動詞"]) and (word in dic):
  print("Bot -> " + make_response(word))
  break

 

辞書を作成、更新することができたので入力されたものから自動作文を作成していく。

入力したものを形態素解析し、単語と品詞情報を持ったwordlistから先頭の要素を取得し感動詞だった時そのままその単語を返し、名詞、形容詞、動詩かつ辞書にその単語が含まれている時、make_response()関数を呼び出し、辞書を元に作成した文章を出力する。

 

make_response()関数、応答文の作成

def make_response(word):
 res = []
 
 # 「名詞」/「形容詞」/「動詞」は、文章の意図を示していることが多いと想定し、始点の単語とする。
 w1 = word
 res.append(w1)
 w2 = word_choice(dic[w1])
 res.append(w2)
 while True:
  # w1,w2の組み合わせから予想される、単語を選択
  if w1 in dic and w2 in dic[w1] : w3 = word_choice(dic[w1][w2])
  else : w3 = ""
  res.append(w3)
  # 文末を表す語の場合、作文を終了
  if w3 == "。" or w3 == "?" or w3 == "?" or w3 == "" : break
  # 次の単語を選択するために、単語をスライド
  w1, w2 = w2, w3
 return "".join(res)

 

呼び出し元のfor文内で入力された文章から先頭の要素してif文で処理を終了しているので名詞、形容詞、動詞は文章の意図を示していることが多いと想定しているため始点の単語としている。

ここではresというリストに辞書を元に単語を追加していき最終的にリストの要素を連結して呼び出し元に返している。

w2=word_choice()dic[w1])ではmake_response()の引数である単語がキーとなるオブジェクトをword_choice()関数に渡して渡した単語に続く単語を取得している。

そして、w1、w2に格納された単語をresに追加。

その後、先頭と先頭から2番目の言葉から予想される単語をword_choice()関数で取得しresに追加していく。

最後にjoinメソッドでresに格納された要素を連結して呼び出し元に返す。

 

word_choice()関数、辞書から取得した要素からランダムに単語を返す

def word_choice(candidate):
 keys = candidate.keys()
 return random.choice(list(keys))

 

引数ではmake_response()関数からキーとなる単語の値となる要素が渡している。

その要素からkeys()メソッドでキーを全て取得し、list()でリストに変換、random.choice()メソッドに渡すことでキーの中からランダムな単語を呼び出し元に返す。

 

 

 

 

 

Doc2Vecの簡単なまとめとマルコフ連鎖

Doc2Vecでは簡単に文章を分類することができる。

単語のベクトルによる計算はWord2Vecを、文章のベクトルによる計算はDoc2Vecと使い分けることができる。

Doc2Vecで作成するプログラムを応用すれば問い合わせの分類や類似度を図るという観点から、「類似した論文が存在しないか」「類似した特許が既に存在しないか」という形で使用することもできる。

モデル作成の際はコーパスの工夫やパラメーターのチューニングによって精度を高めることができる。

 

マルコフ連鎖について

これまでは文章や単語の意味を理解することに焦点を置いてきたが(形態素解析)、次に文章を作成することについて触れていく。

マルコフ連鎖というのは未来の状態が現在の状態のみで決まるという性質を持つ確率過程のこと。つまり過去の状態に関しては加えず、現在の状態でのみ未来を予測するというものである。これを利用することで既存の文章から自動で文章を生成することができる。

流れとしては

・入力された文章を単語に分解する(形態素解析)

・辞書を作成する。

・始点となる単語と辞書を使って、作文する。

これの流れで実行していく。

 

 

Doc2Vecで作成したモデルで作者の分類

作者の分類をするプログラム。

前提として事前に青空文庫から取得した文書データを使用してDoc2Vecでモデルの作成を行っている。そのモデルを使用して作者の分類を行っていく。

import urllib.request as req
import zipfile
import os.path
import MeCab
from gensim import models

#Mecabの初期化
mecab.parse("")

#保存したDoc2Vec学習モデルを読み込み --- (*7)
model = models.Doc2Vec.load('aozora.model')

#分類用のZipファイルを開き、中の文書を取得する --- (*8)
def read_book(url, zipname):
  if not os.path.exists(zipname):
    req.urlretrieve(url, zipname)

  with zipfile.ZipFile(zipname,"r") as zf:
    for filename in zf.namelist():
     with zf.open(filename,"r") as f:
      return f.read().decode("shift-jis")

#引数のテキストを分かち書きして配列にする
def split_words(text):
  node = mecab.parseToNode(text)
  wakati_words =
  while node is not None:
   hinshi = node.feature.split(",")[0]
   if hinshi in ["名詞"]:
    wakati_words.append(node.surface)
   elif hinshi in ["動詞", "形容詞"]:
    wakati_words.append(node.feature.split(",")[6])
  node = node.next
  return wakati_words

#引数のタイトル、URLの作品を分類する --- (*9)
def similar(title, url):
  zipname = url.split("/")[-1]
 
  words = read_book(url, zipname)
  wakati_words = split_words(words)
  vector = model.infer_vector(wakati_words)
  print("--- 「" + title + '」 と似た作品は? ---')
  print(model.docvecs.most_similar([vector],topn=3))
  print("")

#各作家の作品を1つずつ分類 --- (*10)
similar("宮沢 賢治:よだかの星",

similar("芥川 龍之介:犬と笛",

similar("ポー エドガー・アラン:マリー・ロジェエの怪事件",

similar("紫式部:源氏物語 06 末摘花",

実行結果

f:id:hanamichi_sukusuku:20210123223054p:plain

結果としてモデルに学習させたものと類似度の高いものを出力している。

モデルの読み込み

model = models.Doc2Vec.load('aozora.model')

 

関数呼び出し、各作家の作品を一つずつ分類

 

引数のタイトル、URLの作品を分類する

def similar(title, url):
zipname = url.split("/")[-1]
 
words = read_book(url, zipname)
wakati_words = split_words(words)
vector = model.infer_vector(wakati_words)
print("--- 「" + title + '」 と似た作品は? ---')
print(model.docvecs.most_similar([vector],topn=3))
print("")

 

ここではread_book()関数で中身の文書を取得し、split_words()関数で引数で受け取った文章を分かち書きにして配列にして返している。

model.infer_vector()では分かち書きデータを渡すことでベクトルを作成している。

model.docvecs.most_similar()では引数に渡したデータの類似度の高いものから3作品を出力している。topn=N(トップからN番目までを出力)model.docvecs[文書名]で文書のベクトル。model.docvecs.most_similar(文書名(ベクトルデータ))で文書の類似度を確認できる。

 

read_bok()関数、zipファイルを開き、中の文書を取得

def read_book(url, zipname):
  if not os.path.exists(zipname):
   req.urlretrieve(url, zipname)

  with zipfile.ZipFile(zipname,"r") as zf:
   for filename in zf.namelist():
    with zf.open(filename,"r") as f:
     return f.read().decode("shift-jis")

 

if not os.path.exists(zipname):で引数に指定したファイル名が存在しなければ関数の引数で受け取ったurlにアクセスしアクセス先のファイルをダウンロードする処理をしている。

with zipfile.ZipFile()ではzipファイルを解凍しfor文で解凍したzipファイルの中身のファイル名を取得してwith zf.open()でファイルを開きf.read().decode(:shift-jis)でファイルの中身を読み込み、Shift-JISでデコードして文書を呼び出し元に返している。

 

split_word()関数、分かち書きにして配列にする

def split_words(text):
node = mecab.parseToNode(text)
wakati_words =
  while node is not None:
    hinshi = node.feature.split(",")[0]
    if hinshi in ["名詞"]:
     wakati_words.append(node.surface)
    elif hinshi in ["動詞", "形容詞"]:
     wakati_words.append(node.feature.split(",")[6])
   node = node.next
  return wakati_words

 

mecab.parseToNode()で文章を渡し、単語、品詞情報を持ったオブジェクトを変数に代入。

while文のなかのif文でストップワードの除去を行い、新しい配列に追加していく。

最後に作成した配列を呼び出し元に返している。

 

 

 

 

 

 

 

 

 

 

Doc2Vecを利用してモデルの作成

import zipfile

import os.path

import urllib.request as req

import MeCab

from gensim import models

from gensim.models.doc2vec import TaggedDocument

 

#Mecabの初期化

mecab = MeCab.Tagger()

mecab.parse("")

 

#学習対象とする青空文庫の作品リスト --- (*1)

list = [

    {"auther":{

        "name":"宮澤 賢治",

        "url":"https://www.aozora.gr.jp/cards/000081/files/"},

     "books":[

        {"name":"銀河鉄道の夜","zipname":"43737_ruby_19028.zip"},

        {"name":"注文の多い料理店","zipname":"1927_ruby_17835.zip"},

        {"name":"セロ弾きのゴーシュ","zipname":"470_ruby_3987.zip"},

        {"name":"やまなし","zipname":"46605_ruby_29758.zip"},

        {"name":"どんぐりと山猫","zipname":"43752_ruby_17595.zip"},

    ]},

    {"auther":{

        "name":"芥川 竜之介",

        "url":"https://www.aozora.gr.jp/cards/000879/files/"},

     "books":[

        {"name":"羅生門","zipname":"127_ruby_150.zip"},

        {"name":"鼻","zipname":"42_ruby_154.zip"},

        {"name":"河童","zipname":"69_ruby_1321.zip"},

        {"name":"歯車","zipname":"42377_ruby_34744.zip"},

        {"name":"老年","zipname":"131_ruby_241.zip"},

    ]},

    {"auther":{

        "name":"ポー エドガー・アラン",

        "url":"https://www.aozora.gr.jp/cards/000094/files/"},

     "books":[

        {"name":"ウィリアム・ウィルスン","zipname":"2523_ruby_19896.zip"},

        {"name":"落穴と振子","zipname":"1871_ruby_17551.zip"},

        {"name":"黒猫","zipname":"530_ruby_20931.zip"},

        {"name":"群集の人","zipname":"56535_ruby_69925.zip"},

        {"name":"沈黙","zipname":"56537_ruby_70425.zip"},

    ]},

    {"auther":{

        "name":"紫式部",

        "url":"https://www.aozora.gr.jp/cards/000052/files/"},

     "books":[

        {"name":"源氏物語 01 桐壺","zipname":"5016_ruby_9746.zip"},

        {"name":"源氏物語 02 帚木","zipname":"5017_ruby_9752.zip"},

        {"name":"源氏物語 03 空蝉","zipname":"5018_ruby_9754.zip"},

        {"name":"源氏物語 04 夕顔","zipname":"5019_ruby_9761.zip"},

        {"name":"源氏物語 05 若紫","zipname":"5020_ruby_11253.zip"},

    ]},

]

 

#作品リストを取得してループ処理に渡す --- (*2)

def book_list():

    for novelist in list:

        auther = novelist["auther"]

        for book in novelist["books"]:

            yield auther, book

        

#Zipファイルを開き、中の文書を取得する --- (*3)

def read_book(auther, book):

    zipname = book["zipname"]

    #Zipファイルが無ければ取得する

    if not os.path.exists(zipname):

        req.urlretrieve(auther["url"] + zipname, zipname)

    zipname = book["zipname"]

    #Zipファイルを開く

    with zipfile.ZipFile(zipname,"r") as zf:

        #Zipファイルに含まれるファイルを開く。

        for filename in zf.namelist():

            # テキストファイル以外は処理をスキップ

            if os.path.splitext(filename)[1] != ".txt":

                continue

            with zf.open(filename,"r") as f:

                #今回読むファイルはShift-JISなので指定してデコードする

                return f.read().decode("shift-jis")

 

#引数のテキストを分かち書きして配列にする ---(*4)

def split_words(text):

    node = mecab.parseToNode(text)

    wakati_words =

    while node is not None:

        hinshi = node.feature.split(",")[0]

        if  hinshi in ["名詞"]:

            wakati_words.append(node.surface)

        elif hinshi in ["動詞", "形容詞"]:

            wakati_words.append(node.feature.split(",")[6])

        node = node.next

    return wakati_words

 

#作品リストをDoc2Vecが読めるTaggedDocument形式にし、配列に追加する --- (*5)

documents =

#作品リストをループで回す

for auther, book in book_list():

    #作品の文字列を取得

    words = read_book(auther, book)

    #作品の文字列を分かち書き

    wakati_words = split_words(words)

    #TaggedDocumentの作成 文書=分かち書きにした作品 タグ=作者:作品名

    document = TaggedDocument(

        wakati_words, [auther["name"] + ":" + book["name"]])

    documents.append(document)

    

#TaggedDocumentの配列を使ってDoc2Vecの学習モデルを作成 --- (*6)

model = models.Doc2Vec(

    documents, dm=0, vector_size=300, window=15, min_count=1)

 

#Doc2Vecの学習モデルを保存

model.save('aozora.model')

 

print("モデル作成完了")

 

-----------------------------------------------------------------------------------

 

このプログラムではZipファイルをダウンロードして、文学作品を読み込んで学習し、学習した結果を[aozora.model](モデルの作成)というファイルに保存するプログラムである。

 

処理の実行順に明記していく

モジュールインポート

import zipfile

import os.path

import urllib.request as req

import MeCab

from gensim import models

from gensim.models.doc2vec import TaggedDocument

 

import zipfile

zipファイルというzipというデータ圧縮のフォーマットで圧縮されたファイルのことで例えば、[aaaaaaaaaa]を[a10]と表現することで7文字少なく表現することができる。このファイルを解凍するためにimport zipfileで標準ライブラリのzipfileを使用可能にしてzipファイルの圧縮、解凍を行えるようにしている。

 

import os.path

os.pathではファイルやディレクトリが指定したパスに存在するかどうか確認できたり、パスからファイル名や拡張子を取得することができるモジュールである。

 

import urllib.request as req

urllibモジュールはpythonでURLを扱うモジュールで、urllib.requestモジュールはHTTPリクエストに特化したモジュールである。

 

from gensim.models.doc2vec import TaggedDocument

Doc2Vecの学習にはTaggedDocumentクラスのオブジェクトが必要なのでインスタンスを作成できるようインポートする。

 

MeCabの初期化

mecab = MeCab.Tagger()

mecab.parse("")

 

学習対象とする青空文庫の作品リストの情報を持ったリストの作成

list = [

    {"auther":{

        "name":"宮澤 賢治",

        "url":"https://www.aozora.gr.jp/cards/000081/files/"},

     "books":[

        {"name":"銀河鉄道の夜","zipname":"43737_ruby_19028.zip"},

        {"name":"注文の多い料理店","zipname":"1927_ruby_17835.zip"},

        {"name":"セロ弾きのゴーシュ","zipname":"470_ruby_3987.zip"},

        {"name":"やまなし","zipname":"46605_ruby_29758.zip"},

        {"name":"どんぐりと山猫","zipname":"43752_ruby_17595.zip"},

    ]},

    {"auther":{

        "name":"芥川 竜之介",

        "url":"https://www.aozora.gr.jp/cards/000879/files/"},

     "books":[

        {"name":"羅生門","zipname":"127_ruby_150.zip"},

        {"name":"鼻","zipname":"42_ruby_154.zip"},

        {"name":"河童","zipname":"69_ruby_1321.zip"},

        {"name":"歯車","zipname":"42377_ruby_34744.zip"},

        {"name":"老年","zipname":"131_ruby_241.zip"},

    ]},

    {"auther":{

        "name":"ポー エドガー・アラン",

        "url":"https://www.aozora.gr.jp/cards/000094/files/"},

     "books":[

        {"name":"ウィリアム・ウィルスン","zipname":"2523_ruby_19896.zip"},

        {"name":"落穴と振子","zipname":"1871_ruby_17551.zip"},

        {"name":"黒猫","zipname":"530_ruby_20931.zip"},

        {"name":"群集の人","zipname":"56535_ruby_69925.zip"},

        {"name":"沈黙","zipname":"56537_ruby_70425.zip"},

    ]},

    {"auther":{

        "name":"紫式部",

        "url":"https://www.aozora.gr.jp/cards/000052/files/"},

     "books":[

        {"name":"源氏物語 01 桐壺","zipname":"5016_ruby_9746.zip"},

        {"name":"源氏物語 02 帚木","zipname":"5017_ruby_9752.zip"},

        {"name":"源氏物語 03 空蝉","zipname":"5018_ruby_9754.zip"},

        {"name":"源氏物語 04 夕顔","zipname":"5019_ruby_9761.zip"},

        {"name":"源氏物語 05 若紫","zipname":"5020_ruby_11253.zip"},

    ]},

]

 

今回は「青空文庫」にあるテキストを利用するので青空文庫には、著作権が消滅した多くの作品が公開されており、ZIP形式でダウンロードできる。

 

作品リストをDoc2Vecが読めるTaggedDocument形式にし、配列を追加する

documents =

#作品リストをループで回す

for auther, book in book_list():

    #作品の文字列を取得

    words = read_book(auther, book)

    #作品の文字列を分かち書き

    wakati_words = split_words(words)

    #TaggedDocumentの作成 文書=分かち書きにした作品 タグ=作者:作品名

    document = TaggedDocument(

        wakati_words, [auther["name"] + ":" + book["name"]])

    documents.append(document)

 

for auther, book in book_list()でbook_list()関数から文学作品の情報を取得し、read_book()関数でZipファイルを開き、文書の文字列を取得する。split_words()で作品の文字列を分かち書きにしてそのデータをTaggedDocument()に渡すことでDoc2Vecを使用したモデルの作成に必要なTaggerdDocumentオブジェクトの配列を作成している。

TaggedDocument(単語文字列, タグ)タグは第一引数で渡した文字列を表す名前。

 

book_list()関数、作品リストを取得してループ処理に渡す関数

def book_list():

    for novelist in list:

        auther = novelist["auther"]

        for book in novelist["books"]:

            yield auther, book

 

この関数では文学作品の情報を持った変数listから作者名、ファイル先のURLを格納したauther、作品名とファイル名が格納してあるbooksを取得し、呼び出し元のループ処理に渡している。

yieldでは関数を一時的に実行停止させることができる機能を持つ文である。その時点での戻り値を返し、また処理を再開させることができる。

returnでは一度に大きなデータを持ったリストなどを返すと一度にたくさんのメモリを消費してしまことになってしまう。yieldによってその都度処理を停止して少量ずつデータを送ることで消費メモリを抑えることができる。

 

read_book()関数、zipファイルを開き、中身の文書を取得する関数

def read_book(auther, book):

    zipname = book["zipname"]

    #Zipファイルが無ければ取得する

    if not os.path.exists(zipname):

        req.urlretrieve(auther["url"] + zipname, zipname)

    zipname = book["zipname"]

    #Zipファイルを開く

    with zipfile.ZipFile(zipname,"r") as zf:

        #Zipファイルに含まれるファイルを開く。

        for filename in zf.namelist():

            # テキストファイル以外は処理をスキップ

            if os.path.splitext(filename)[1] != ".txt":

                continue

            with zf.open(filename,"r") as f:

                #今回読むファイルはShift-JISなので指定してデコードする

                return f.read().decode("shift-jis")

 

book_list()関数から渡されたauther(作者、ファイル先のURL),books(作品名、ファイル名)を引数として受け取っている。

 

  if not os.path.exists(zipname):では指定したパスが存在しているかどうかの確認を行っている。ファイルでもディレクトリでも存在すればTrue、存在しなければFalseを返す。

 

req.urlretrive()はネット上からファイルをダウンロードし保存するために使う。

urlretrive(目的のURL, 保存先のファイル名)と指定する。

 

with zipfile.ZipFile(zipname,"r") as zf:ではzipファイルを解凍し、zfに格納している。

for filename in zf.namelist():のzf.namelist()ではzipファイルの中身のファイルを取得することができる。

 

if os.path.splitext(filename)[1] != ".txt":

                continue

このif文でテキストファイル以外の時処理をスキップしている。os.path.splitext()ではファイルやフォルダ名から拡張子を取得することができるメソッドである。

 

with zf.open(filename,"r") as f:で処理したファイル名を指定してファイルを開いている。

 

return f.read().decode("shift-jis")ではread()でファイルを読み込んで、decode()では読み込むファイルはShift-JISなので指定してデコードしている。元のデータ形式から変換することを「エンコード」、エンコードされた形式から元の形式に戻すことをデコードという。今回は読み込んだファイルがShift-JISで書かれたものなのでそれに合わせた処理をして呼び出し元にファイルの中身の文字列を返している。

 

split_words()関数、引数のデータを分かち書きにして配列にする

def split_words(text):

    node = mecab.parseToNode(text)

    wakati_words =

    while node is not None:

        hinshi = node.feature.split(",")[0]

        if  hinshi in ["名詞"]:

            wakati_words.append(node.surface)

        elif hinshi in ["動詞", "形容詞"]:

            wakati_words.append(node.feature.split(",")[6])

        node = node.next

    return wakati_words

 

引数で受け取った文字列をMeCabを使用して分かち書きににして配列にしている。

if文で条件分岐することでストップワードの除去を行っている。

 

TaggedDocumentの配列を使ってDoc2Vecの学習モデルを作成

model = models.Doc2Vec(

    documents, dm=0, vector_size=300, window=15, min_count=1)

 

第一引数にTaggedDocumentの配列, dmはDec2Vecで使用するアルゴリズムの選択(1=dmpw、0=DBOW), vector_sizeはベクトルの次元の設定でDec2Vecでは基本的に300がいいとされている, windowは学習する単語の前後数(DROWでは15がいいとされている), min_countは最低何回出てきた文字列を対象とするかの設定(今回は作家ごとに独特の言い回しがあると考えられるので、一回でも出てきた文字列を対象にしている)

 

 

 

 

学習モデル保存

model.save('aozora.model')

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Word2Vecの簡単なまとめ

まとめ

前回までではWikiprdiaの文章をモデルに学習させたが、テキストの集合体であればなんでも学習させることができる。

例えば標準語と方言の類似度を調べることもできる。

・今日も寒いから暖かい格好をしてね

・今日はしばれるから暖かい格好をしてね

このようなコーパスがあれば「寒い」と「しばれる北海道弁)」の意味が類似していることがわかるようになる。

またWord2Vecで単語のベクトル計算など行う時はコーパスの工夫も必要だが、モデル作成時のパラメーターによるチューニングが結果に対して影響を及ぼすので時間をかけて試すか、独自の仕組みを考えていくかしていくことで精度をあげる必要がある。

 

・gensimモジュールを使うことで単語のベクトル化、文章のジャンル分けなどの処理を行うことができる。

・Word2Vecによりベクトル化した単語の情報は、機械学習や計算に利用することができる。

・単語のベクトル化は自然言語処理の幅を広げる。

 

次回からはDoc2Vecについて学習していく

Doc2VecはWord2Vecを発展させたもので任意の文章をベクトル化できる。

Doc2Vecにも二つのアルゴリズムがありdmpwは精度重視、DBOWが速度重視になる。

 

 

 

 

文章から指定した文字との類似度を調べる

f:id:hanamichi_sukusuku:20210120212844p:plain

実行結果

f:id:hanamichi_sukusuku:20210120212911p:plain

このプログラムは文章から任意の文字との類似度を出力するプログラムである。

今回の場合、「至急」という文字との類似度を確認できることで文章の緊急度合いがわかるようになる。

実行結果を見ると一つ目の文章の一部が数値が高くなっている。おそらく「急いで」部分で数値が高くなっている。

Word2Vecのモデルとmecabの用意

model = word2vec.Word2Vec.load("./wiki.model")
tagger = MeCab.Tagger("-d /var/lib/mecab/dic/mecab-ipadic-neologd")
tagger.parse("")

 

以前作成したwiki.modelというwikipediaのデータから作成したモデルを読み込み。

tagger.parse("")を記述することでMeCabで使用している標準の文字エンコードに初期化しているんだと思う(一応この記述なくても出力は同じだけど)

 

関数の呼び出し

print_emargency("PCが起動しなくなりました。急いでいます。")
print_emargency("使い方がよくわかりません。")

 

渡されたテキストに含まれる各単語と「至急」の類似度を表示

def print_emargency(text):
  print(text)
  #渡されたテキストを形態素解析
  node = tagger.parseToNode(text)
  while node is not None:
  #ストップワードを除く
  fields = node.feature.split(",")
  if fields[0] == '名詞' or fields[0] == '動詞' or fields[0] == '形容詞':
  #至急との類似度を表示する
     print(model.wv.similarity(node.surface, '至急'))
  node = node.next

 

tagger.parseToNode()でnodeにsurface(単語)feature(品詞情報)を持つ解析結果を代入している。parseToNode()の返り値は単語、品詞情報を持ったオブジェクトを返す。

 

node.feature.split(",")では品詞情報を取得している。このメソッドでは単語の表層形以外のものを取得しており品詞情報が文字列で取得されるのでsplit(",")でリストを作成している。

そしてif文で変数fieldsの先頭の要素(品詞)を取得して名詞、動詞、形容詞の時に「至急」との類似度を出力している。

 

model.wv.similarity()では指定した単語の類似度を評価できる。単語、品詞情報を持ったオブジェクトnodeからsurface(表層形を取得するメソッド)で表層形を取得して「至急との類似度を出力。

 

node=node.nextをすることで次の要素の処理に進める。これをしないと無限ループになる。