959
580

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

AIが三国志を読んだら、孔明が知力100、関羽が武力99、を求められるのか?をガチで考える物語(自然言語処理編)

Last updated at Posted at 2019-07-28

背景

関羽「どれどれ、拙者たち英傑の活躍は後世では
   どのように伝えられているのかな?」
孔明「なんとっ・・!!?扇からビーム出しとる?
   そしてSDガンダムと融合しとる!?
   あまつさえ、女体化して萌キャラなっとる!?」

"この報告は孔明にとってはショックだった・・・"

劉禅「いや、オマイラは知力100だったり、
   武力99だったりして優遇されとるだろ。
   朕なんて101匹いても勝てないぞ」
魏延「オレ、ゲンシジン、ミタイ、ナッテル・・・」

孔明「いや、わたしが知力100なのは当然でしょ」
司馬懿「まてぃ。最後に勝ったのはワシだよ?」
荀彧「違います。私こそが王佐の才・・・」

甘寧「最強はこの鈴の甘寧」
張遼「遼来々!最強はワタシだ!」
張飛「オレっちを忘れちゃいないかい!?」

誰が一番、武力・知力が高いのか
英傑たちの議論は白熱していった・・・。

曹操「みなの衆、静まれいっ!!
   ちかごろは、えーあいなるものがあると聞く。
   わしは有能なものは泥棒でも使ってやるぞ。
   えーあいに聞いてみようではないか!?」

本投稿の趣旨

KOEIの武将ステータスに大きな敬意を払いつつ、

三国志の小説を 自然言語解析 & 機械学習 すると
各武将のステータスはどのようになるのか?
の実験&研究を行う物語。

まさに技術の無駄の無双乱舞
(そして無駄に長い背景)

Colaboratoryを使って、環境構築不要でブラウザだけで
誰でも本格的な「三国志分析」が出来るという、
誰得コダワリ技のご紹介。
(※ふつーの自然言語処理の技としても流用可)

出来るだけ、コピペだけでお手元でも試していただけるように書く予定。

結論の一部を先に見てみよう

注:左から順に「武力、知力、政治、魅力」

武将名 本実験の推論結果 (参考)KOEI三国志5データ
曹操 95, 92, 87, 105 87, 96, 97, 98
劉備 89, 89, 84, 105 79, 77, 80, 99
諸葛亮 78, 98, 90, 104 60, 100, 96, 97
関羽 92, 75, 62, 82 99, 83, 64, 96
張飛 97, 61, 44, 77 99, 45, 17, 44
魏延 91, 65, 50, 68 94, 48, 37, 56
袁紹 70, 71, 66, 77 81, 77, 49, 92

吉川英治の「三国志」@青空文庫をINPUTとして、
「自然言語処理」と「機械学習」によって上記のように、
武力や知力などのパラメータを推論する。

三国志小説の機械学習結果として、
1つの武将を50次元ベクトルに変換し、そのベクトルを、
全く同じ「式」に入れて出てきた値が、上記の表。

このような方法:「小説(自然言語)」⇒「数値化」⇒「式」
によって、武力/知力を求めることが出来るか?
という実験&研究が今回のテーマ。

他の成果としては、
以下のような武将名の「演算」が楽しめる。
(これも実際の出力結果より抜粋)

  • 諸葛亮に近い人は誰?
    • ⇒ 姜維、司馬懿、陸遜、周瑜、魏延、馬謖
  • 劉備にとっての関羽は、曹操にとって誰?
    • ⇒ 袁紹、張遼
    •  ※若いころの馴染み的な意味や対比が多いので袁紹?
  • 孫権にとっての魯粛は、劉備にとって誰?
    • ⇒ 司馬徽(水鏡先生)、徐庶
    •  ※賢者を紹介するポジションなのか?

精度の高い結果を得るためには、前提として、
三国志という特殊な小説を、
うまーく自然言語処理(の前処理)をすることが最重要。
草履売りから蜀漢皇帝になるように、処理の改善のたびに、
コードが三国志を征服していくような物語を楽しんでほしい。

なお、機械学習の結果は面白いけれども、自然言語側から、
しかも1つの小説だけから作るのは精度に限度があるため、
本当にゲームのパラメータを決めたいならば、
INPUTとなる小説やテキストを大量に用意することが望ましい。
(このような手法が可能かどうか?を実験する目的であり、
 実際にパラメータをコレで決めたいわけではない)

天下三分の計 ~全体方針/目次~

曹操「えーあい?、えーあい?・・・」
楊修「おk、把握した。全軍退却!!」
劉備「待てぃ。話が終わってしまうw」
楊修「じゃあ劉備殿は えーあい が分かるのですかな?」
劉備「ぐっ! 孔明! 任せた、あとよろ!」
孔明「・・・。」
劉備「あとよろ! あとよろ!」
孔明「では天下三分の計の如く、
   3つのステップで今回の計画をご説明しんぜよう」
劉備「(3回言わないとやってくれないんだもんな・・・)」

■今回の進め方は以下3つのステップである。

① 吉川英治「三国志」@青空文庫を、
 三国志の固有名称に気をつけて、
 形態素解析し単語単位にバラす。

② バラした結果をWord2Vecによって、ベクトル化する。
 (Word2Vec:単語をN次元のベクトルで表現でき、
   その足し算引き算等の演算が行える技術。
   「赤の他人」の対義語は「白い恋人」 これを自動生成したい物語
   https://qiita.com/youwht/items/f21325ff62603e8664e6
  を先に見て頂くと良いかもしれない)

③ それぞれの「武将」がベクトル化された状態になるため、
  その中から「武力」や「知力」と相関が高いような
  ベクトル(複数ベクトルの集合体)を見つければ、
  何らかの数式によって、KOEI三国志のパラメータに
  近いものが計算できるのではないか?

一番最初にして最大の難関は、①の形態素解析、
三国志の世界を出来るだけ正しく認識すること。

以下のような、三国志の世界独特の壁が立ちはだかる。

  • 韓玄,劉度,趙範,金旋「我ら荊州四英傑をえーあいは分かるかな?」
  • 玄徳=劉玄徳=劉備玄徳 ⇒ 「劉備」のこと
※いらすとやさんの劉備の画像(あるんですね!)

ではさっそく①形態素解析から始めよう!

桃園の誓い ~環境準備~

"我ら生まれた時は違えども、死すべき時は同じと願わん!"

今回義兄弟の誓いをたてる最強のツールは以下3点。

  • Colaboratory (ブラウザ上で無料で使えるPython実行環境)
  • Janome  (環境構築が超楽な形態素解析器)
  • Word2Vec  (自然言語を数値化/ベクトル化する仕組み)

まずは、ColaboratoryとJanomeで、
一番簡単な自然言語処理の仕組みを作ってみる。
(ブラウザだけでお手元で簡単に試せます)

Colaboratoryの準備

Colaboratory (要Googleアカウント)
にアクセス。基本的な使い方はぐぐってくだされぃ。
環境構築不要でブラウザだけでプログラミングが出来る。

「ファイル」⇒「Python3の新しいノートブック」を作成しよう。

GoogleDriveに今回使う様々なデータを保存したいので、
下記のコマンドでGoogleDriveをマウントしよう。

GoogleDriveのマウント
# これを実行すると、認証用URLが表示されて、キーを入力すると
# 「drive/My Drive/」の中に、認証したアカウントのgoogle driveが入る
from google.colab import drive
drive.mount('/content/drive')

日本語を区切って品詞判定などが出来る、
Janome をインストールする。
Colaboratoryでは、コマンドの冒頭に「!」を書くことで、
いわゆるシェルコマンドが実行できる。

Janomeのインストール
!pip install janome

さっそく、Janomeで名詞・動詞の抽出をしてみよう!

Janomeで形態素解析(名詞・動詞の抽出)
#素状態のJanomeの性能を確認する
# Janomeのロード
from janome.tokenizer import Tokenizer

# Tokenneizerインスタンスの生成 
tokenizer = Tokenizer()

# テキストを引数として、形態素解析の結果、名詞・動詞原型のみを配列で抽出する関数
def extract_words(text):
    tokens = tokenizer.tokenize(text)
    return [token.base_form for token in tokens 
        if token.part_of_speech.split(',')[0] in['名詞', '動詞']]

sampletext = u"文章の中から、名詞、動詞原型などを抽出して、リストにするよ"
print(extract_words(sampletext))
sampletext = u"劉備と関羽と張飛の三人は桃園で義兄弟の契りを結んだ"
print(extract_words(sampletext))
sampletext = u"悪来典韋はかえって、許褚のために愚弄されたので烈火の如く憤った"
print(extract_words(sampletext))
実行結果
['文章', '', '名詞', '動詞', '原型', '抽出', 'する', 'リスト', 'する']
['', '', '', '', '', '', '', '', '桃園', '義兄弟', '契り', '結ぶ']
['', '', '', '', 'ため', '愚弄', 'する', 'れる', '烈火', '憤る']

ここまででもう、最も簡単な自然言語処理をする環境が整った!!
しかし結果をよ~く見てみると・・・。

げぇっ!関羽! ~武将名識別①~

げぇっ!「関羽」
が認識されていない・・・

関羽 ⇒ '関', '羽'
とバラバラになっている。「義兄弟」などの一般名詞と違い、
「劉備」「関羽」「張飛」などの三国志の武将名は、
普通に実行するだけでは認識されないのだ。

桃園の義兄弟レベルの人名が認識されないなんて大したことないな。
いやいや、Janomeではmecab-ipadic-NEologdの辞書データを使える。

Janomeの作者様 (@moco_beta 様) によって、
mecab-ipadic-NEologdを同梱したパッケージを公開していただいている。
(大感謝!温州蜜柑を差し上げたい
以下のURLにアクセスして、自分のGoogleDriveにコピーしよう。

https://drive.google.com/drive/folders/0BynvpNc_r0kSd2NOLU01TG5MWnc
(右クリックですぐにコピー、自分のGoogleDriveに持ってこれる)

janome+neologdのインストール
#結構時間がかかる(6分くらい)
#Mydrive上の、先程のjanome+neologdのパスを指定する
#最新版とファイル名が一致しているかどうかは各自で確認すること
!pip install "drive/My Drive/Janome-0.3.9.neologd20190523.tar.gz" --no-compile

インストールは成功した、かに見えるが、
最後に以下のような記載が出て、
「RESTART RUNTIME」のボタンが出る。

インストール実行結果の末尾
#WARNING: The following packages were previously imported in this runtime:
#  [janome]
#You must restart the runtime in order to use newly installed versions.

ColaboratoryのRUNTIMEを一度リセットしてね、
というお話なので、このボタンを押せばOK

Janomeの作者様の公式の方法はローカル環境向けであるため、
python -c "from janome.tokenizer import Tokenizer; Tokenizer(mmap=True)"
↑このコマンドを実行することになっているようだが、
Colaboratoryでは、RUNTIMEリセットすればこのコマンドは不要。

NEologd同梱版では、最初のTokenneizerインスタンスの生成コードだけ
ちょっと変える必要がある。
以下のコードで、NEologdの効果を見てみよう!

NEologd入れた状態で形態素解析する

# Janomeのロード
from janome.tokenizer import Tokenizer

# Tokenneizerインスタンスの生成 ★ここが異なる★
tokenizer = Tokenizer(mmap=True)

# テキストを引数として、形態素解析の結果、名詞・動詞原型のみを配列で抽出する関数
def extract_words(text):
    tokens = tokenizer.tokenize(text)
    return [token.base_form for token in tokens 
        if token.part_of_speech.split(',')[0] in['名詞', '動詞']]


sampletext = u"劉備と関羽と張飛の三人は桃園で義兄弟の契りを結んだ"
print(extract_words(sampletext))
sampletext = u"悪来典韋はかえって、許褚のために愚弄されたので烈火の如く憤った"
print(extract_words(sampletext))
sampletext = u"田豊。沮授。許収。顔良。また――審配。郭図。文醜。などという錚々たる人材もあった。"
print(extract_words(sampletext))
sampletext = u"第一鎮として後将軍南陽の太守袁術、字は公路を筆頭に、第二鎮、冀州の刺史韓馥、第三鎮、予州の刺史孔伷、第四鎮、兗州の刺史劉岱、第五鎮、河内郡の太守王匡、第六鎮、陳留の太守張邈、第七鎮、東郡の太守喬瑁"
print(extract_words(sampletext))
実行結果
['劉備', '関羽', '張飛', '', '', '桃園', '義兄弟', '契り', '結ぶ']
['悪来', '典韋', '許褚', 'ため', '愚弄', 'する', 'れる', '烈火', '憤る']
['田豊', '沮授', '', '', '顔良', '審配', '郭図', '文醜', '錚々たる', '人材', 'ある']
['', '後将軍', '南陽', '太守', '袁術', '', '公路', '筆頭', '', '', '冀州', '刺史', '', '', '', '', '予州', '刺史', '', '', '', '', '兗州', '刺史', '', '', '第五', '', '河内郡', '太守', '王匡', '', '', '', '', '太守', '', '', '', '', '東郡', '太守', '', '']

劉備、関羽、張飛はもちろんのこと、
典韋、許褚、田豊、沮授、などが認識出来ていることが分かる。
また、こうした有名武将の認識以外の面でも、
動詞や一般名詞の認識精度も上がるため、全体的に望ましい結果になる。

だがこの結果をよーく見てみると・・・・。

反董卓連合の全滅 ~武将名識別②~

NEologdを導入することで「劉備」「関羽」などの
ステータスが90以上ありそうな人や、SSRになっていそうな人
は認識出来るようになったが、
三国志の世界にはまだまだ有名ではないコモン扱いの人々は沢山居る。

先の結果では、裏切者の代名詞:「許収」が認識されていない。
また、タピオカ入り蜜水が大好きなニセ皇帝「袁術」さんは認識されたが、
韓馥、孔伷、劉岱、張邈、喬瑁、は全滅である。
これでは反董卓連合の激文を書くことができない。

さすがのNEologdでもここまではカバーしていなかったのだ。

そこで、「三国志登場人物リスト」を作って、
ユーザ辞書」としてJanomeに登録することにした。

https://ja.wikipedia.org/wiki/三国志演義の人物の一覧
このページの人物一覧をもとに、単純に1行に1名ずつ書いたテキストを作る。
それをアップロードして、以下のように読み込んでみよう。

人名リストの読み込み
#人物の名前が列挙してあるテキストから、ワードリストを作成する
import codecs
def getKeyWordList():
    input_file = codecs.open('drive/My Drive/Sangokusi/三国志_人名リスト.txt' , 'r', 'utf-8')
    lines = input_file.readlines() #読み込み
    result_list = []
    for line in lines:
        tmp_line = line
        tmp_line = tmp_line.replace("\r","")
        tmp_line = tmp_line.replace("\n","")
        #ゴミデータ削除のため、2文字以上のデータを人名とみなす
        if len(tmp_line)>1:
            
            result_list.append(tmp_line)
    return result_list
  
jinbutu_word_list = getKeyWordList()
print(len(jinbutu_word_list))
print(jinbutu_word_list[10:15])
実行結果
1178
['張楊', '張虎', '張闓', '張燕', '張遼']

このように、1178名分の人物を入れた、単純なリストを得た。

なお、マニアックな調整点や考慮点として、
「馬忠」は同姓同名がいるため、その区別はあきらめたり、
「喬瑁」はwikiに居なかったので後で追加したり、
「張繍」「張繡」の微妙な字体の違いとか、
「祝融夫人」⇒「祝融」に変更したりなどの調整はしている。

このリストをもとに、Janomeで利用可能な、
「ユーザ辞書形式」のCSVファイルを作成する。
設定できる箇所は多いのだが、今回は単純な人名リストであるため、
全部同じ登録内容で楽をする。

Janomeのユーザ辞書csvの作成

#作成したキーワードリストから、janomeのユーザ辞書形式となるCSVファイルを作成する
keyword_list = jinbutu_word_list
userdict_list = []

#janomeのユーザ辞書形式に変換をかける。コストや品詞の設定等
for keyword in keyword_list:
  #「表層形,左文脈ID,右文脈ID,コスト,品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用型,活用形,原形,読み,発音」
  #参考:http://taku910.github.io/mecab/dic.html
  #コストは,その単語がどれだけ出現しやすいかを示しています. 
  #小さいほど, 出現しやすいという意味になります. 似たような単語と 同じスコアを割り振り, その単位で切り出せない場合は, 徐々に小さくしていけばいい
  
  userdict_one_str = keyword + ",-1,-1,-5000,名詞,一般,*,*,*,*," + keyword + ",*,*"
  #固有名詞なので、かなりコストは低く(その単語で切れやすく)設定
  userdict_one_list = userdict_one_str.split(',')
  userdict_list.append(userdict_one_list)

print(userdict_list[0:5])

#作成したユーザ辞書形式をcsvでセーブしておく
import csv
with open("drive/My Drive/Sangokusi/三国志人名ユーザ辞書.csv", "w", encoding="utf8") as f:
  csvwriter = csv.writer(f, lineterminator="\n") #改行記号で行を区切る
  csvwriter.writerows(userdict_list)
実行結果
[['張譲', '-1', '-1', '-5000', '名詞', '一般', '*', '*', '*', '*', '張譲', '*', '*'], ['張角', '-1', '-1', '-5000', '名詞', '一般', '*', '*', '*', '*', '張角', '*', '*'], ['張宝', '-1', '-1', '-5000', '名詞', '一般', '*', '*', '*', '*', '張宝', '*', '*'], ['張梁', '-1', '-1', '-5000', '名詞', '一般', '*', '*', '*', '*', '張梁', '*', '*'], ['張飛', '-1', '-1', '-5000', '名詞', '一般', '*', '*', '*', '*', '張飛', '*', '*']]

これで、有名(?)武将1000名以上が掲載されたユーザ辞書を得ることが出来た!
いよいよこの辞書を適用した結果を試してみよう。

ユーザ辞書を使った場合
# Janomeのロード
from janome.tokenizer import Tokenizer

#ユーザ辞書、NEologd 両方使う。★ここが変更点★
tokenizer_with_userdict = Tokenizer("drive/My Drive/Sangokusi/三国志人名ユーザ辞書.csv", udic_enc='utf8', mmap=True)

# テキストを引数として、形態素解析の結果、名詞・動詞原型のみを配列で抽出する関数
def extract_words_with_userdict(text):
    tokens = tokenizer_with_userdict.tokenize(text)
    return [token.base_form for token in tokens 
        #どの品詞を採用するかも重要な調整要素
        if token.part_of_speech.split(',')[0] in['名詞', '動詞']]

sampletext = u"劉備と関羽と張飛の三人は桃園で義兄弟の契りを結んだ"
print(extract_words_with_userdict(sampletext))
sampletext = u"悪来典韋はかえって、許褚のために愚弄されたので烈火の如く憤った"
print(extract_words_with_userdict(sampletext))
sampletext = u"田豊。沮授。許収。顔良。また――審配。郭図。文醜。などという錚々たる人材もあった。"
print(extract_words_with_userdict(sampletext))
sampletext = u"第一鎮として後将軍南陽の太守袁術、字は公路を筆頭に、第二鎮、冀州の刺史韓馥、第三鎮、予州の刺史孔伷、第四鎮、兗州の刺史劉岱、第五鎮、河内郡の太守王匡、第六鎮、陳留の太守張邈、第七鎮、東郡の太守喬瑁"
print(extract_words_with_userdict(sampletext))
実行結果
['劉備', '関羽', '張飛', '', '', '', '桃園', '義兄弟', '契り', '結ぶ']
['悪来', '典韋', '許褚', 'ため', '愚弄', 'する', 'れる', '烈火', '憤る']
['田豊', '沮授', '', '', '顔良', '審配', '郭図', '文醜', '錚々たる', '人材', 'ある']
['', '後将軍', '南陽', '太守', '袁術', '', '公路', '筆頭', '', '', '冀州', '刺史', '韓馥', '', '', '予州', '刺史', '孔伷', '', '', '兗州', '刺史', '劉岱', '第五', '', '河内郡', '太守', '王匡', '', '', '', '', '太守', '張邈', '', '', '東郡', '太守', '喬瑁']

「ウムッ!」

かなり三国志のコダワリを入れた結果が得られた!!

もちろん荊州四英傑のデータも入れているため、
弱小君主たちもそのファンも納得の分析が出来る。

余談:
形態素解析を行う場合、まず出てくる候補はmecabであろう。
しかし、mecabは環境構築が結構難しく大変である。
Colaboratory上ですぐに使う方法も知られてはいるが、
じゃあ、neologd入れられる?ユーザ辞書自分で追加できる?
となると、なかなかWeb上だけではサクサク環境構築出来ないと思う。
その点でJanomeは環境構築ハードルを下げてくれるので超オススメ!
三国志などの独自世界に対応した超カスタマイズ自然言語処理環境を
作る方法としては、おそらく最も扱いやすい手順を得られたと思う。
作者様ありがとうございます☆  温州蜜柑を差し上げたい。2つ目

一見するともうこれで十分だろ、感があるが、
まだまだ敵は立ちはだかる。
いよいよ次は「孔明の罠」にハマる物語。

その前に、ちょっと疲れてきたので休憩を兼ねて、
ここでスポンサーの曹操様から、
CM(イベントのご案内)を入れさせていただこう!

突然ですが、CMです☆

「SEKIHEKIのたた会」イベント案内

日時 : 208年11月20日頃
     (東南の風がふくまでご自由にご歓談ください)
場所 : 赤壁
参加者: 曹操・周瑜・諸葛亮など豪華ゲストが続々登壇!
LT  : 孫権 「部下がまとまる机の切り方」
     黄蓋 「三代の功臣が若手に無茶振りされた話」
     諸葛亮「10万本の矢を集めたノウハウを大公開」
     蔡瑁 「転職直後に上司の信頼を得る方法」
     龐統 「絶対に船酔いしない基盤構築を教えます」
     曹操 「部下を生き生きと働かせるアジャイル風マネジメント」
その他: 懇親会あり。あの有名武将と人脈を作るチャンス☆
     (寝返り目的の参加はご遠慮ください)

ここまで読んでいる人(居るのか?)には垂涎のイベント。
ぜひみなさまお誘いあわせの上ご参加ください!!

曹操「赤壁の戦いでお会いしましょう!(※ただし関羽テメーはダメだ)」

なお、ここまでで吉川英治三国志に興味を持った方は、
下記の速読アプリにも全巻無料で登録されていマス。
訓練不要で誰でも速読!日本一の速読アプリ「瞬間速読」の個人開発物語
残念ながらSEKIHEKIイベントにご参加できなかった方は、
こちらのアプリでイベントの様子を見ていただくことが出来ます。

さあ、いよいよ次は孔明の罠の登場だ。

「孔明」の罠 ~字(あざな)識別~

やった、人名データを登録したからこれで解析が出来るぞ!

待てあわてるなこれは「孔明」の罠だ。

このまま解析しても良い結果は得られない。
次の例文を見ていただこう。

「車上、白衣簪冠の人影こそ、まぎれなき諸葛亮孔明にちがいなかった。」
「これは予州の太守劉玄徳が義弟の関羽字は雲長なり」
趙子龍は、白馬を飛ばして、馬上から一気に彼を槍で突き殺した。」
趙雲子龍も、やがては、戦いつかれ、玄徳も進退きわまって、すでに自刃を覚悟した時だっ

「孔明」とは字(あざな)であり、「諸葛亮」が本名である。
彼は通常「孔明」と表現されているが、
諸葛亮、や、諸葛亮孔明、と表現されていることもたびたびある。
また、
「劉備」の字(あざな)は「玄徳」
「関羽」の字(あざな)は「雲長」
「趙雲」の字(あざな)は「子龍」
であり、文中でも「玄徳は~~」「雲長は~~」などと
たびたび字(あざな)が登場する。

このように、三国志の世界では、同じ人物に対して、
様々な呼び方が存在している。

少なくとも以下の4パターンは同じ人物として扱わないと困る。
「趙雲」=「子龍」=「趙子龍」=「趙雲子龍」
「劉備」=「玄徳」=「劉玄徳」=「劉備玄徳」
江東の小覇王とか、劉皇叔とか、は一旦忘れる。

これが、世に名高い**「孔明(あざな)」の罠**。
ハマると同じ人物が4分裂してしまう凶悪な罠だ。

この罠を回避するために、まずは
字(あざな)と武将名のリストを作成し、
字をフルネームに変える置換処理を作る。

さらに、単純に置換しただけでは、
「趙子龍」⇒「趙趙雲」
「趙雲子龍」⇒「趙雲趙雲」
となってしまうため、これらの重複防止措置を取る。

なお、字(あざな)で書かれる場合が多いのは
かなり有名な武将に限定されているため、
今回用意した字リストは約130人分までだ。
このくらいまでなら、適宜三国志のファンサイトを参照して作成可能だ。
単純にカンマ区切りで、あざな&フルネームのCSVを作成し、読み込む。

あざなCSVの読み込み
import csv

csv_file = open("drive/My Drive/Sangokusi/三国志_あざな変換リスト.csv", "r", encoding="utf8", errors="", newline="" )
#リスト形式
azana_reader = csv.reader(csv_file, delimiter=",", doublequote=True, lineterminator="\r\n", quotechar='"', skipinitialspace=True)
azana_list = [ e for e in azana_reader ]
csv_file.close()

print(len(azana_list))
print(azana_list[2])

#全員の字リストを作るのは難しかったが、
#['雲長', '関羽']のような132人の代表的な字とその対比表が入っている
実行結果
132
['雲長', '関羽']

このようにして作成した対比表を用いて、
テキストに対する字(あざな)の変換処理を作る。

字(あざな)の変換処理の実装
#これは、字(あざな)を置き換えるだけの単純な置換処理
def azana_henkan(input_text):
    result_text = input_text
    for azana_pair in azana_list:
        result_text = result_text.replace(azana_pair[0],azana_pair[1])
    return result_text

#単純に、字からの変換をかけるだけだと、
#趙雲子龍→趙雲趙雲などのようになる場合が多いため、
#同一の人物名で重複している場合は、一方を削除する。
#また、劉玄徳、趙子龍、などのような表現に対応するため、
#フルネームで2文字の場合はAAB→AB(劉玄徳→劉劉備→劉備)
#フルネームで3文字の場合はAAB→AB(諸葛孔明→諸葛諸葛亮→諸葛亮)
# となる名寄せを行う。
#(※名字1文字+名前二文字はあまり居ない気がするので無視)
def jinmei_tyouhuku_sakujyo(input_text):
    jinbutu_word_list = getKeyWordList()
    result_text = input_text
    for jinbutumei in jinbutu_word_list:
        result_text = result_text.replace(jinbutumei+jinbutumei, jinbutumei)
        if len(jinbutumei) == 2:
            result_text = result_text.replace(jinbutumei[0]+jinbutumei, jinbutumei)
        if len(jinbutumei) == 3:
            result_text = result_text.replace(jinbutumei[0]+jinbutumei[1]+jinbutumei, jinbutumei)
    return result_text

sampletext = u"これは予州の太守劉玄徳が義弟の関羽字は雲長なり"
print(jinmei_tyouhuku_sakujyo(azana_henkan(sampletext)))
sampletext = u"趙子龍は、白馬を飛ばして、馬上から一気に彼を槍で突き殺した。"
print(jinmei_tyouhuku_sakujyo(azana_henkan(sampletext)))
sampletext = u"趙雲子龍も、やがては、戦いつかれ、玄徳も進退きわまって、すでに自刃を覚悟した時だった。"
print(jinmei_tyouhuku_sakujyo(azana_henkan(sampletext)))
実行結果
これは予州の太守劉備が義弟の関羽字は関羽なり
趙雲は白馬を飛ばして馬上から一気に彼を槍で突き殺した
趙雲もやがては戦いつかれ劉備も進退きわまってすでに自刃を覚悟した時だった

「ウムッ!」

やっと、三国志の固有名詞と、孔明の罠に対応することが出来た!

いよいよ三国統一の最後のツメとして、
漢中攻略に向かおう
前処理としては最後の関門に向かう。

鶏肋は死刑に ~ストップワード除去~

曹操「鶏肋、鶏肋・・・」
楊修「おk、把握した」

「鶏肋」とは、食べるには身がないがダシが取れるので
そのまま捨てるには惜しいことから
大して役に立たないが、捨てるには惜しいもの」のこと。

「雲長は気の毒になって、の好きな酒を出して与えたが」
↑ここで言う「彼」はもちろん「張飛」のこと。
しかし別なシーンでは、「彼」は「曹操」や「劉備」かもしれない。

この「彼」を除かずに分析を行うと、
「曹操」≒「彼」のような分析結果が出てしまう。
(単純に曹操が登場回数が多いこともあり)

一見意味のありそうな「彼」だが、
実際解析する上では雑音にしかならない。
よって、楊修と同じように死刑にしてしまおう

曹操「何勝手に退却しているんだよw死刑!」

このような鶏肋ワードの一覧として、良く使われるのが、
SlothLib というサイトだ。

ここに乗っている単語は全て死刑(削除)にするコードを書く。

まず、SlothLibにアクセスしてそのデータをリスト化する。

SlothLibからのデータの取得&リスト化
#雑音になりやすい単語(「彼」など)はストップワードとして除外する
#SlothLibのテキストを使う。
#どんな言葉が除外されるのかは、直接URLを見れば良い
#参考: http://testpy.hatenablog.com/entry/2016/10/05/004949
import urllib
slothlib_path = 'http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt'
#slothlib_file = urllib2.urlopen(slothlib_path) #←これはPython2のコード
slothlib_file = urllib.request.urlopen(slothlib_path)
slothlib_stopwords = [line.decode("utf-8").strip() for line in slothlib_file]
slothlib_stopwords = [ss for ss in slothlib_stopwords if not ss==u'']

#['彼','彼女',・・・]のようなリストになる
print(len(slothlib_stopwords))
print(slothlib_stopwords[10:15])
実行結果
310
['いま', 'いや', 'いろいろ', 'うち', 'おおまか']

310個の鶏肋ワードを取得することが出来た。
このようにして出来たリストを使って、
名詞動詞の抽出後に、鶏肋リスト中の単語は除外する処理、を実装しよう。

鶏肋ワードの除去機能を実装する
sampletext = u"彼は予州の太守劉玄徳が義弟の関羽字は雲長。彼は劉備玄徳の義兄弟だ"

tmp_word_list = extract_words_with_userdict(jinmei_tyouhuku_sakujyo(azana_henkan(sampletext)))

print(tmp_word_list)

#このようにして、単語リストからストップワードを除外する
tmp_word_list = [word for word in tmp_word_list if word not in slothlib_stopwords]

print(tmp_word_list)
実行結果
['', '予州', '太守', '劉備', '義弟', '関羽', '', '関羽', 'なり', '', '劉備', '', '義兄弟']
['予州', '太守', '劉備', '義弟', '関羽', '関羽', 'なり', '劉備', '', '義兄弟']

上下の抽出結果を比べてみると、
「彼」という単語が消えているのが分かる。

さあ、全ての準備が整った。
最後にこれらの成果を吉川英治の全文に適用してみよう。

ジャーンジャーンジャーン(全文の形態素解析)

これまでの全ての成果を全文に適用する時が来た。

まず、「青空文庫」から吉川英治三国志の全文をダウンロードし、
全部の章を結合したテキストを作っておく。

ここで注意しなければならないのは、
以下のような青空文庫の独自表記。

公孫※[#「王+贊」、第3水準1-88-37]《こうそんさん》

⇒「公孫瓚」に置換しておく必要がある。

私は以下のような変換コードを1万行くらい書いてある、
独自のコードを以前から使っているが、もっといいやり方がある気がする。
self.resulttext=re.sub(r'※[#.*?1-88-37.*?]',"瓚",self.resulttext)

既に本稿が長くなりすぎており、
これは三国志というか青空文庫ハッキングの話であるため、
本稿においては割愛させていただく。

このような変換処理をかけた全文テキストデータを用意した所から話を続ける。

まず、字(あざな)の名寄せを行う。

全文テキストに対して、字(あざな)変換処理をかける
import codecs
def azana_henkan_from_file(input_file_path):
    input_file  = codecs.open(input_file_path, 'r', 'utf-8')
    lines = input_file.readlines() #読み込み
    result_txt = ""
    for line in lines:
        result_txt += line
    result_txt = azana_henkan(result_txt)
    return result_txt

#ファイル生成用関数定義
#mesのテキストを、filepathに、utf-8で書き込む
def printFile(mes,filepath):
    file_utf = codecs.open(filepath, 'w', 'utf-8')
    file_utf.write(mes)
    file_utf.close()
    return "OK"

azana_henkango_zenbun = azana_henkan_from_file('drive/My Drive/Sangokusi/三国志全文.txt')
azana_henkango_zenbun = jinmei_tyouhuku_sakujyo(azana_henkango_zenbun)

printFile(azana_henkango_zenbun,'drive/My Drive/Sangokusi/三国志全文_あざな変換済み.txt')

これで生成された字(あざな)変換済みのテキストに対して、
NEologd、ユーザ辞書、を搭載したJanomeによる形態素解析を行おう。
出来たデータは、pickleを使ってGoogleDrive内に保存しておけば、
引き続き作業を行う時に楽になる。

全文の形態素解析
%%time
#全文分解するのに10分ほどかかる

import codecs
# ['趙雲', '白馬', '飛ばす', '馬上', '彼', '槍', '突き', '殺す'] このようなリストのリスト(二次元リスト)になる
def textfile2wordlist(input_file_path):
    input_file  = codecs.open(input_file_path, 'r', 'utf-8')
    lines = input_file.readlines() #読み込み
    result_word_list_list = []
    for line in lines:
        # 1行ずつ形態素解析によってリスト化し、結果格納用のリストに格納していく
        # Word2Vecでは、分かち書きされたリスト=1文ずつ、のリストを引数にしている
        tmp_word_list = extract_words_with_userdict(line)
        
        #別途準備しておいたstopワードリストを使って除外処理を行う
        tmp_word_list = [word for word in tmp_word_list if word not in slothlib_stopwords]
        
        result_word_list_list.append(tmp_word_list)
    return result_word_list_list

Word_list_Sangokusi_AzanaOK_with_userdict_neologd = textfile2wordlist('drive/My Drive/Sangokusi/三国志全文_あざな変換済み.txt')

#作成したワードリストは、pickleを使って、GoogleDriveに保存しておく(一回10分くらいかかるからね)
import pickle
with open('drive/My Drive/Sangokusi/Word_list_Sangokusi_AzanaOK_with_userdict_neologd_V4.pickle', 'wb') as f:
    pickle.dump(Word_list_Sangokusi_AzanaOK_with_userdict_neologd, f)

#保存したpickleファイルは、以下のように復元する
with open('drive/My Drive/Sangokusi/Word_list_Sangokusi_AzanaOK_with_userdict_neologd_V4.pickle', 'rb') as f:
    Word_list_Sangokusi_AzanaOK_with_userdict_neologd = pickle.load(f)
    
print(len(Word_list_Sangokusi_AzanaOK_with_userdict_neologd))
print(Word_list_Sangokusi_AzanaOK_with_userdict_neologd[10:20])

これでとうとう、
吉川英治三国志全文を解析し、
武将名をかなり正しく認識&名寄せした上で、
「名詞、動詞」のリストに変換することが出来た!

次の作戦は、出来たリストを機械学習にかけ、
抽出された「武将名」の学習を行うことだ。
(次回へ続く・・・?)

自然言語処理編の終わり

仲達「こんなに長い記事を書いているなんて、
   フフフ、諸葛亮も長くはないぞ!」

長くなりすぎた。
キリも良いので、作者と読者の健康のために一旦ここまでで切る。
CMとかやってるからだよ

三国志の世界を機械学習するためには、
今回実施したようなコダワリの前加工処理が精度向上の鍵になる。

関羽千里行なみに、各関門をなぎ倒していく物語はいかがだっただろうか?

また、Colaboratory + Janome + NEologd + ユーザ辞書、
まで全セットの使い方として、
自然言語処理の裾野開拓にお役に立つことがあれば幸いである。
(Web上で簡単に作れて、NEologd+ユーザ辞書、まで使えるノウハウは、
 かなり調べても全て説明しているものは見当たらなかったため)

以前より、プログラマ向けに対象を限定せず、
非プログラマ/非Qiitaユーザでも雰囲気は楽しめるレベル、を
イメージして記事を投稿してきたが、
今回については、「三国志」知らない人には意味不明であろう。
Qiitaにも「三国志」タグは無かった・・・(当たり前)

「SEKIHEKIのたた会」イベントの参加者はQiita見て無さそうだし、
この投稿も「孔明の罠」って書きたかっただけだし、
「機械学習編」は書かないかもしれない。

後半が気になる人や、三国志分析が面白かったという人、
横山光輝リスペクトの部分でニヤっとした人は、
ぜひ応援よろしくお願いします。
後半では各英傑たちの「主人公補正」が明らかになるかも!?

★追記:続編(Word2Vec編)書きました。
https://qiita.com/youwht/items/fb366579f64252f7a35c

★追記:完結編、書きました。
https://qiita.com/youwht/items/61c6d5819cdc3aff9e63

長文おつきあいありがとうございました。
以上です。

959
580
27

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
959
580

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?