13
5

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 3 years have passed since last update.

GMOアドマーケティングAdvent Calendar 2020

Day 13

文章から誰の文章かを判別してみた

Last updated at Posted at 2020-12-12

1.はじめに

大学の時、犯罪捜査に文章の解析を使用した教授がいらっしゃっいました。
曰く、人の文章の癖は読点に出るそうです。
青空文庫の小説で読点の前の文字の割合を比較してみました。

2.やること

著作権切れの小説が青空文庫というサイトに保存されています。
ここから夏目漱石と太宰治の作品について特徴を取って比較します。
ここからの内容は全てこちらのColabで実行できます。

3.mecabのインストール

形態素解析を行うmecabというソフトがありますのでインストールします。
今回mecabは文章を区切るため使用します。

# mecabインストール
apt-get install mecab  
apt-get install libmecab-dev
apt-get install mecab-ipadic-utf8
pip install mecab-python3 > /dev/null
# python3-mecabに必要
pip install unidic-lite

4.青空文庫のデータ取得

青空文庫の作品が解凍され置いてあるリポジトリがあり、そちらをcloneします。

git clone --depth 1 https://github.com/aozorahack/aozorabunko_text.git

夏目漱石の作品は以下で調べることができます。

# 夏目漱石(000148)のファイル取得
find aozorabunko_text/cards/000148/files/ -name '*.txt'
=======================================================================
aozorabunko_text/cards/000035/files/42362_txt_34277/42362_txt_34277.txt
aozorabunko_text/cards/000035/files/52461_txt_45424/52461_txt_45424.txt
aozorabunko_text/cards/000035/files/52685_ruby_45085/52685_ruby_45085.txt
aozorabunko_text/cards/000035/files/1605_ruby_18026/1605_ruby_18026.txt
aozorabunko_text/cards/000035/files/45688_ruby_21351/45688_ruby_21351.txt
...略

card/以下の数字が作家を示しています。

太宰治(00035)も同様に行います。

夏目漱石、太宰治それぞれについて、作品が重複しているファイルがあるのでxxxx_ruby_xxxx.txtという形式のファイルのみlistに代入します。

# 重複がありそうなファイルをいくつか上記結果から手作業で削除したのちlistへ
list_natume="""
aozorabunko_text/cards/000148/files/2675_ruby_6355/2675_ruby_6355.txt
aozorabunko_text/cards/000148/files/764_ruby_2927/764_ruby_2927.txt
aozorabunko_text/cards/000148/files/4689_txt_9473/4689_txt_9473.txt
...略
aozorabunko_text/cards/000148/files/47148_ruby_32216/47148_ruby_32216.txt
aozorabunko_text/cards/000148/files/769_ruby_565/769_ruby_565.txt
""".split()

5.余計な文字列の削除

ファイル名から中身のファイルを取り出す関数を作成します。

# ファイルの中身取得
def read_string_from_file(filename):
  with open(filename, encoding="shift_jis") as f:
    file_string = f.read()
  return file_string

text = read_string_from_file("aozorabunko_text/cards/000148/files/56923_ruby_66668/56923_ruby_66668.txt")
text
===================================================================================
門\n夏目漱石\n\n-------------------------------------------------------\n【テキスト中に
現れる記号について】\n\n《》:ルビ\n(例)宗助《そうすけ》\n\n|:ルビの付く文字列の始まりを特定する記号\n(例
)二三|分《ぷん》\n\n[#]:入力者注\u3000主に外字の説明や、傍点の位置の指定\n\u3000\u3000\u3000(
数字は、JIS X 0213の面区点番号またはUnicode、底本のページと行数)\n(例)※[#「金+饌のつくり」、第4水準2
-91-37]\n\n/\:二倍の踊り字(「く」を縦に長くしたような形の繰り返し記号)\n(例)ぎら/\\n*濁点付きの二倍
...略

ファイルの中身には不要な文字列やルビが振ってあるので削除します。
こちらを参考にしました。

# 余計な文字列を除去する
import re
def format(text):
  text = re.split(r'\-{5,}', text)[2]
  text = re.split(r'底本:', text)[0]
  text = re.sub(r'《.+?》', '', text)
  text = re.sub(r'[#.+?]', '', text)
  text = re.sub(r'\u3000', '', text)
  text = re.sub(r'\r\n', '', text)
  text = text.strip()
  return text

formatted_text = format(text)
formatted_text
================================================================================
一\n\n宗助は先刻から縁側へ坐蒲團を持ち出して日當りの好ささうな所へ氣樂に胡坐をかいて見たが、やがて手に持
つてゐる雜誌を放り出すと共に、ごろりと横になつた。秋日和と名のつく程の上天氣なので、徃來を行く人の下駄の響が
、靜かな町丈に、朗らかに聞えて來る。肱枕をして軒から上を見上ると、奇麗な空が一面に蒼く澄んでゐる。其空が自分

6.特徴の抽出

mecabで文章を分かち書きする関数を作成します。
例えば「太郎はりんごを買った」は「太郎 は りんご を 買 った」となります。

def owakati(text):
  import MeCab
  mecab = MeCab.Tagger("-Owakati")
  owakati_text = mecab.parse(text)
  return owakati_text

owakati_text = owakati(formatted_text)
owakati_text
========================================================================
一 宗助 は 先刻 から 縁側 へ 坐 蒲 團 を 持ち出し て 日 當 り の 好さ さう な 所 へ 氣 樂 に 胡坐 
を かい て 見 た が 、 やがて 手 に 持つ て ゐる 雜誌 を 放り出す と 共 に 、 ごろり と 横 に なつ た
 。 秋日和 と 名 の つく 程 の 上天 氣 な の で 、 徃來 を 行く 人 の 下駄 の 響 が 、 靜 か な 町
...略

その後N_gramを作成します。
N_gramとは連続するN個のまとまりを抜き出したものです。
今回読点の前の文字が知りたいのでN=2です。

def two_gram(owakati_text):
  owakati_list = owakati_text.split()
  two_gram_list = list(zip(owakati_list[:-1], owakati_list[1:]))
  return two_gram_list

two_gram_list = two_gram(owakati_text)
two_gram_list
===========================================================================
[('一', '宗助'),
 ('宗助', 'は'),
 ('は', '先刻'),
 ('先刻', 'から'),
 ('から', '縁側'),
 ('縁側', 'へ'),
 ('へ', '坐'),
 ('坐', '蒲'),
 ('蒲', '團'),
...略

N_gramから読点の前の文字をカウントします。

from collections import Counter
def count_before_comma(two_gram_list):
  before_comma = map(lambda pair: pair[0] ,(filter(lambda pair: pair[1] == "、", two_gram_list)))
  count_before_comma = Counter(before_comma)
  count_before_comma_list = count_before_comma.most_common()
  # type: count_before_comma_list = list(tuple(particle, count))
  return count_before_comma_list

count_before_comma_list = count_before_comma(two_gram_list)
count_before_comma_list
================================================================================
[('て', 1199),
 ('が', 444),
 ('で', 422),
 ('は', 419),
 ('に', 413),
 ('と', 375),
...略

読点全体のうち、["が","は","て","で","と","も","に"]の文字が読点の前にくる割合を求めました。
これらの文字にした理由は特にないですが頻度が多いものを選びました。

def particle_ratio_before_comma(count_before_comma_list, particles):
  before_comma_sum = sum([i[1] for i in count_before_comma_list])
  features = {}
  for particle in particles:
    particle_sum = [i[1] for i in count_before_comma_list if i[0] == particle][0]
    ratio =  particle_sum / before_comma_sum 
    features[particle] = ratio
  return features

particles = ["が", "は", "て", "で", "と", "も", "に"]
features = particle_ratio_before_comma(count_before_comma_list, particles)
features
================================================================================
{'が': 0.08951612903225807,
 'て': 0.24173387096774193,
 'で': 0.08508064516129032,
 'と': 0.07560483870967742,
 'に': 0.08326612903225807,
 'は': 0.0844758064516129,
 'も': 0.038306451612903226}

7.作家の全ての作品に対して特徴抽出

上記の作業を一つの関数にまとめ、ファイル名から特徴を抽出する関数を作成します。
4.でリストに入れた作家の全ての作品に対して特徴を出してみます。

def get_features_from_filename(filename):
  try:
    text = read_string_from_file(filename)
    formatted_text = format(text)
    if len(formatted_text) < 3000:
      return None

    particles = ["が", "は", "て", "で", "と", "も", "に"]
    owakati_text = owakati(formatted_text)
    two_gram_list = two_gram(owakati_text)
    count_before_comma_list = count_before_comma(two_gram_list)
    features = particle_ratio_before_comma(count_before_comma_list, particles)
    return features
  except IndexError:
    return None

こちらは夏目漱石の作品の特徴です。

for i in list_natume:
  if get_features_from_filename(i) != None:
    print(get_features_from_filename(i))
===============================================================================
{'が': 0.0898876404494382, 'は': 0.056179775280898875, 'て': 0.20224719101123595, 'で': 0.10112359550561797, 'と': 0.19101123595505617, 'も': 0.02247191011235955, 'に': 0.0449438202247191}
{'が': 0.08337122934667272, 'は': 0.09822646657571624, 'て': 0.22964984083674397, 'で': 0.06866757617098682, 'と': 0.06199787782325299, 'も': 0.04577838411399121, 'に': 0.07306351371835683}
{'が': 0.041666666666666664, 'は': 0.06666666666666667, 'て': 0.10833333333333334, 'で': 0.15833333333333333, 'と': 0.05, 'も': 0.05, 'に': 0.041666666666666664}
{'が': 0.1111111111111111, 'は': 0.07407407407407407, 'て': 0.1728395061728395, 'で': 0.09876543209876543, 'と': 0.08641975308641975, 'も': 0.012345679012345678, 'に': 0.037037037037037035}
{'が': 0.07166301969365427, 'は': 0.061269146608315096, 'て': 0.2696936542669584, 'で': 0.06181619256017506, 'と': 0.12035010940919037, 'も': 0.02516411378555799, 'に': 0.0836980306345733}
{'が': 0.0981012658227848, 'は': 0.12025316455696203, 'て': 0.13291139240506328, 'で': 0.04430379746835443, 'と': 0.060126582278481014, 'も': 0.07278481012658228, 'に': 0.06962025316455696}
{'が': 0.08028169014084507, 'は': 0.07370892018779343, 'て': 0.17934272300469484, 'で': 0.036619718309859155, 'と': 0.03192488262910798, 'も': 0.03333333333333333, 'に': 0.04178403755868545}
{'が': 0.058823529411764705, 'は': 0.029411764705882353, 'て': 0.19117647058823528, 'で': 0.03676470588235294, 'と': 0.051470588235294115, 'も': 0.022058823529411766, 'に': 0.051470588235294115}
{'が': 0.04990757855822551, 'は': 0.04436229205175601, 'て': 0.17005545286506468, 'で': 0.033271719038817, 'と': 0.025878003696857672, 'も': 0.022181146025878003, 'に': 0.06099815157116451}
{'が': 0.09302325581395349, 'は': 0.10077519379844961, 'て': 0.11627906976744186, 'で': 0.13953488372093023, 'と': 0.031007751937984496, 'も': 0.031007751937984496, 'に': 0.06976744186046512}
{'が': 0.05405405405405406, 'は': 0.05405405405405406, 'て': 0.18018018018018017, 'で': 0.05405405405405406, 'と': 0.02702702702702703, 'も': 0.13513513513513514, 'に': 0.018018018018018018}
{'が': 0.07964601769911504, 'は': 0.09734513274336283, 'て': 0.10176991150442478, 'で': 0.07079646017699115, 'と': 0.05752212389380531, 'も': 0.061946902654867256, 'に': 0.05309734513274336}
{'が': 0.12582781456953643, 'は': 0.06622516556291391, 'て': 0.07947019867549669, 'で': 0.1456953642384106, 'と': 0.026490066225165563, 'も': 0.06622516556291391, 'に': 0.013245033112582781}
{'が': 0.07042253521126761, 'は': 0.11267605633802817, 'て': 0.11267605633802817, 'で': 0.07042253521126761, 'と': 0.08450704225352113, 'も': 0.056338028169014086, 'に': 0.028169014084507043}
{'が': 0.0856353591160221, 'は': 0.024861878453038673, 'て': 0.11602209944751381, 'で': 0.03591160220994475, 'と': 0.11049723756906077, 'も': 0.008287292817679558, 'に': 0.049723756906077346}
{'が': 0.09195402298850575, 'は': 0.05747126436781609, 'て': 0.19540229885057472, 'で': 0.10344827586206896, 'と': 0.19540229885057472, 'も': 0.022988505747126436, 'に': 0.04597701149425287}
{'が': 0.059975767366720514, 'は': 0.10056542810985461, 'て': 0.1732633279483037, 'で': 0.034531502423263326, 'と': 0.0444264943457189, 'も': 0.02968497576736672, 'に': 0.09753634894991922}
{'が': 0.08059610705596107, 'は': 0.06873479318734793, 'て': 0.20072992700729927, 'で': 0.041970802919708027, 'と': 0.06143552311435523, 'も': 0.043491484184914844, 'に': 0.0705596107055961}
{'が': 0.08951612903225807, 'は': 0.0844758064516129, 'て': 0.24173387096774193, 'で': 0.08508064516129032, 'と': 0.07560483870967742, 'も': 0.038306451612903226, 'に': 0.08326612903225807}
...略

こちらは太宰治の作品の特徴です。

for i in list_dazai:
  if get_features_from_filename(i) != None:
    print(get_features_from_filename(i))
===================================================================================
{'が': 0.05277777777777778, 'は': 0.225, 'て': 0.13333333333333333, 'で': 0.044444444444444446, 'と': 0.011111111111111112, 'も': 0.07777777777777778, 'に': 0.019444444444444445}
{'が': 0.05497106785902157, 'は': 0.15123619147816939, 'て': 0.1165176223040505, 'で': 0.035507627564439766, 'と': 0.023934771173066808, 'も': 0.07417148869016307, 'に': 0.039978958442924775}
{'が': 0.04, 'は': 0.2, 'て': 0.16444444444444445, 'で': 0.017777777777777778, 'と': 0.013333333333333334, 'も': 0.04888888888888889, 'に': 0.04}
{'が': 0.04697986577181208, 'は': 0.16778523489932887, 'て': 0.09172259507829977, 'で': 0.06487695749440715, 'と': 0.035794183445190156, 'も': 0.07829977628635347, 'に': 0.02237136465324385}
{'が': 0.034576888080072796, 'は': 0.13466787989080983, 'て': 0.12829845313921748, 'で': 0.030937215650591446, 'と': 0.020928116469517744, 'も': 0.06824385805277525, 'に': 0.04094631483166515}
{'が': 0.06896551724137931, 'は': 0.17980295566502463, 'て': 0.10591133004926108, 'で': 0.06157635467980296, 'と': 0.03694581280788178, 'も': 0.059113300492610835, 'に': 0.029556650246305417}
{'が': 0.0647985989492119, 'は': 0.24430823117338005, 'て': 0.09719789842381786, 'で': 0.040280210157618214, 'と': 0.023642732049036778, 'も': 0.08231173380035026, 'に': 0.0691768826619965}
{'が': 0.058765915768854066, 'は': 0.188050930460333, 'て': 0.11459353574926542, 'で': 0.03525954946131244, 'と': 0.013712047012732615, 'も': 0.06953966699314397, 'に': 0.04995102840352596}
{'が': 0.0570281124497992, 'は': 0.2176706827309237, 'て': 0.11485943775100402, 'で': 0.02570281124497992, 'と': 0.016867469879518072, 'も': 0.06265060240963856, 'に': 0.04257028112449799}
{'が': 0.0773286467486819, 'は': 0.17398945518453426, 'て': 0.10369068541300527, 'で': 0.028119507908611598, 'と': 0.026362038664323375, 'も': 0.056239015817223195, 'に': 0.05272407732864675}
{'が': 0.05013927576601671, 'は': 0.13649025069637882, 'て': 0.15041782729805014, 'で': 0.036211699164345405, 'と': 0.03899721448467967, 'も': 0.07799442896935933, 'に': 0.055710306406685235}
{'が': 0.056010928961748634, 'は': 0.20673952641165755, 'て': 0.11065573770491803, 'で': 0.028233151183970857, 'と': 0.027777777777777776, 'も': 0.07377049180327869, 'に': 0.052367941712204005}
{'が': 0.07391910739191074, 'は': 0.21199442119944212, 'て': 0.12273361227336123, 'で': 0.040446304044630406, 'と': 0.012552301255230125, 'も': 0.06555090655509066, 'に': 0.04881450488145049}
{'が': 0.016113744075829384, 'は': 0.05781990521327014, 'て': 0.07203791469194312, 'で': 0.015165876777251185, 'と': 0.009478672985781991, 'も': 0.03222748815165877, 'に': 0.01990521327014218}
...略

「が」の後に読点が来る割合が夏目漱石のほうが高いのが分かりますね。また「は」は太宰治の方が多いようです。
読点には作者の個性が出ているのが分かると思います。

機械学習モデルで識別

最後にロジスティック回帰で識別してみました。
詳細は割愛させていただきますが、Colabの方には載せたのでもし良ければ。
AUCは0.986と高かったので、十分特徴が得られていると言えるでしょう。

おわりに

小さい頃、読点は読みやすいところにうてと言われ、全然どこにうっていいか分からなかった記憶が蘇りました。
明日は、Ryota-Yokouchiさんによる「Railsで作った脆弱性をOWASP ZAPで診断してみる」です。

引き続き、GMOアドマーケティング Advent Calendar 2020 をお楽しみください!

13
5
0

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
13
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?