恒星間ボトルメール

Interstellar Message in a Bottle

『レ・ミゼラブル』を素材としてテキスト分析・生成をしよう

テキスト分析・生成

本ブログでは、過去記事を対象に自然言語処理NLP)を行っている。

今回、NLPのスキルを磨くために、『人気ブロガー からあげ先生のとにかく楽しいAI自作教室』を参考にしてテキスト分析を行った。

『とにかく楽しいAI自作教室』は、はてなブログで人気を博しているブロガー・からあげさんによる技術書である。AI初心者が、AI技術を実践しながら学べる構成になっている。

www.nikkeibp.co.jp

本書の第3章の内容は、夢野久作ドグラ・マグラ』を題材にテキスト分析・生成だった。今回は、そのコードを流用させて『レ・ミゼラブル』のテキストを分析した。

レ・ミゼラブル

レ・ミゼラブル』は、ヴィクトル・ユーゴーが執筆した小説である。19世紀前半のフランスを舞台に、ジャン・バルジャンの生涯が描かれている。

www.iwanami.co.jp

青空文庫から、『レ・ミゼラブル』第四部「叙情詩と叙事詩 プリューメ街の恋歌とサン・ドゥニ街の戦歌」(https://www.aozora.gr.jp/cards/001094/card42603.html)をダウンロードし、分析対象とした。

1. テキストデータの前処理

1~3の内容は、このgoogle colabファイル(https://colab.research.google.com/drive/1VD_x-Zdnprbz7Czm98Yg6ALX0_F6caGI?usp=sharing)に入っている。

1では、テキストをダウンロードし、1行1行読み込んで、テキストのリストを作成した。

次に、青空文庫特有の注釈形式を正規表現で指定してルビや注釈を消去し、「それ」や「よう」のような一般的な単語も除去。janome形態素解析を行い、名詞だけを取り出した。

!wget https://github.com/aozorabunko/aozorabunko/raw/master/cards/001094/files/42603_ruby_26028.zip
!unzip 42603_ruby_26028.zip

text_list = []
with open('07jojoshito_jojishi.txt', encoding='shift_jis') as f:
  text_list = f.readlines()
  # 1行1行を要素とするリスト

!pip install janome

!apt -y install fonts-ipafont-gothic
!pip install wordcloud

import re
def normalize_text(text):
  text = re.sub(r'([#割り注].*?[#割り注終わり])', '', text)
  text = re.sub(r'《.*?》', '', text)
  text = re.sub(r'[#.*?]', '', text)
  text = re.sub(r'\u3000', '', text)
  text = re.sub(r'それ', "", text)
  text = re.sub(r'よう', "", text)
  text = re.sub(r'もの', "", text)
  text = re.sub(r'こと', "", text)
  text = re.sub(r'うち', "", text)
  text = re.sub(r'これ', "", text)
  text = re.sub(r'ここ', "", text)
  text = re.sub(r'もの', "", text)
  text = re.sub(r'そう', "", text)
  text = re.sub(r'ため', "", text)
  text = re.sub(r'そこ', "", text)
  text = re.sub(r'\n', '', text)
  text = text.strip()
  return text

new_text_list = []
for text in text_list:
  text = normalize_text(text)
  new_text_list.append(text)

new_new_text_list = new_text_list[25:-15]

from janome.tokenizer import Tokenizer

t = Tokenizer()
words = []
for text in new_new_text_list:
  tokens = t.tokenize(text)
  for token in tokens:
    pos = token.part_of_speech.split(',')[0]
    if pos == '名詞':
      words.append(token.surface)

text = ' '.join(words)

2. WordCloudによる可視化

分かち書きされた文章を用いて、WordCloudで頻出単語を可視化した。

from wordcloud import WordCloud
import matplotlib.pyplot as plt

fpath = 'usr/share/fonts/opentype/ipafont-gothic/ipagp.ttf'
wordcloud = WordCloud(background_color = "white", font_path=fpath, width=900, height=500).generate(text)
plt.figure(figsize=(15, 12))
plt.imshow(wordcloud)
plt.axis('off')
plt.show()

f:id:emrdkn:20211208212430p:plain
wordcloud

3. Word2Vecによる可視化

skip-gram形式でword2vecの学習を行い、各単語を300次元の単語ベクトルに変換した。

各単語ベクトルの情報をvector.tsvに保存し、各単語ベクトルが何の単語に対応するかの情報をmetadata.tsvに保存した。

これらのファイルをベクトル可視化サイト(https://projector.tensorflow.org/)にアップロードし、単語ベクトルをPCAによって可視化した。

filename = "wakachi_text.txt"
with open(filename, 'w', encoding='utf-8') as f:
  f.write(text)
print("file is saved")

from gensim.models import word2vec
import logging
logging.basicConfig(
    format = '%(asctime)s : %(levelname)s : %(message)s',
    level=logging.INFO
)
sentences = word2vec.LineSentence('wakachi_text.txt')
model = word2vec.Word2Vec(
    sentences,
    sg=1,
    size=300,
    window=5,
    min_count=5
)

model.save("./word2vec.model")

with open('vector.tsv', 'w', encoding='utf-8') as f, \
  open('metadata.tsv', 'w', encoding='utf-8') as g:
  for word in model.wv.vocab.keys():
    embedding = model.wv[word]

    # save vector TSV file
    f.write('\t'.join([('%f' % x) for x in embedding]) + '\n')

    # save metadata TSV file
    g.write(word + '\n')

from google.colab import files
files.download('vector.tsv')
files.download('metadata.tsv')

f:id:emrdkn:20211208212710p:plain
vector visualization

4. マルコフ連鎖による文章生成

4では、4階マルコフ連鎖モデルでテキストデータを学習し、『レ・ミゼラブル』に似た文章を生成した。

google colabファイルはこちら(https://colab.research.google.com/drive/1ZUsKizQguG1bfBr07G6bye9H5m4ZovhM?usp=sharing)。

!wget https://github.com/aozorabunko/aozorabunko/raw/master/cards/001094/files/42603_ruby_26028.zip
!unzip 42603_ruby_26028.zip

text_list = []
with open('07jojoshito_jojishi.txt', encoding='shift_jis') as f:
  text_list = f.readlines()

import re
def normalize_text(text):
  text = re.sub(r'([#割り注].*?[#割り注終わり])', '', text)
  text = re.sub(r'《.*?》', '', text)
  text = re.sub(r'[#.*?]', '', text)
  text = re.sub(r'〔', '', text)
  text = re.sub(r'〕', '', text)
  text = re.sub(r'\u3000', '', text)
  text = re.sub(r'それ', "", text)
  text = re.sub(r'よう', "", text)
  text = re.sub(r'もの', "", text)
  text = re.sub(r'こと', "", text)
  text = re.sub(r'うち', "", text)
  text = re.sub(r'これ', "", text)
  text = re.sub(r'ここ', "", text)
  text = re.sub(r'もの', "", text)
  text = re.sub(r'そう', "", text)
  text = re.sub(r'ため', "", text)
  text = re.sub(r'そこ', "", text)
  text = re.sub(r'\n', '', text)
  text = text.strip()
  return text

new_text_list = []
for text in text_list:
  text = normalize_text(text)
  new_text_list.append(text)

new_new_text_list = new_text_list[25:-15]

!pip install janome

from janome.tokenizer import Tokenizer

def wakachigaki(text_list):
  t = Tokenizer()
  words = []
  for text in text_list:
    toknes = t.tokenize(text)
    for token in toknes:
      pos = token.part_of_speech.split('.')[0]
      words.append(token.surface)
  text = ' '.join(words)
  return text

word_list = [w for w in wakachigaki(new_new_text_list).split()]

def make_markov_model_4(word_list):
  markov = {}
  w1 = ''
  w2 = ''
  w3 = ''
  w4 = ''
  for word in word_list:
    if w1 and w2 and w3 and w4:
      if (w1, w2, w3, w4) not in markov:
        markov[(w1, w2, w3, w4)] = []
      markov[(w1, w2, w3, w4)].append(word)
    w1, w2, w3, w4 = w2, w3, w4, word
  return markov

markov_model_4 = make_markov_model_4(word_list)

import random

def generate_text_4(model, max_sentence):
  count_sentence=0
  sentence = ''
  w1, w2, w3, w4 = random.choice(list(model.keys()))

  while count_sentence < max_sentence:
    try:
      tmp = random.choice(model[(w1, w2, w3, w4)])
      sentence += tmp
      if(tmp=='。'):
        count_sentence += 1
        sentence += '\n'
      w1, w2, w3, w4 = w2, w3, w4, tmp
    except:
      w1, w2, w3, w4 = random.choice(list(model.keys()))
  
  return sentence

print(generate_text_4(markov_model_4, 10))

実際に生成した文章がこちらである。乱数を使用しているため、実行のたび出力される文章は異なる。

逆立てたらの者は、空高く、真実とも思えないのに、日を送っていた。 彼はリュクサンブールへやって来る時間を変え、ベンチを変え、ハンケチを置いてゆき、また一人でやってきたりした。 マリユスはもう金を持たなかったので、身の破滅を感じてる悲惨な絶望的な注意を配って、上衣の後ろのポケットにそっと手を差し入れ、財布をつかみ、手を引き出し、そしてまたはいながら、蛇が逃げるに闇の中にいて、彼の警戒の念をいっ強めたのであった。 三人に敬意を欠いた笑い方だった。 「足ぬきにはもってこいの黒んぼ(夜)だ!」とアンジョーラは言った、「なに大したでもないがね。 マレンゴーでは首に二個所サーベルの傷を受け、アウステルリッツでは右の腕に弾を受け、フリートラントでは銃剣の傷を受け、アウステルリッツでは右の腕に弾を受け、イエナでは左の腰にやはり弾を受け、フリートラントでは銃剣の傷を受け、アウステルリッツでは右の腕に弾を受け、フリートラントでは銃剣の傷を受け、ルーツェンでは榴弾の破片で指を一本くじいた。 ……私は死ぬ。 私はあなたを愛する。 をあなたが読む頃には、衣裳は短かすぎるになる。 「かわいだな!」とガヴローシュは言った、「この庭は私のである。

まとめ

正規表現を活用して、テキストの前処理を行う。word2vecにより単語ベクトルを作ることができる。

文章生成を実現するモデルの一つとして、マルコフ連鎖モデルがある。

今後の展望

『とにかく楽しいAI自作教室』には、さらに多くの自然言語処理の手法が紹介されている。今後、これらを学び、過去記事解析に生かしたい。

参考資料

  • からあげ『人気ブロガー からあげ先生のとにかく楽しいAI自作教室』(日経BP、2021年)