唯物是真 @Scaled_Wurm

プログラミング(主にPython2.7)とか機械学習とか

pixivの小説を機械学習で男性向けと女性向けに分類する

最近Web小説が人気でいろいろ書籍化されたりアニメ化したりしています
今期のアニメでは『ダンジョンに出会いを求めるのは間違っているだろうか』が放送されていておすすめです(アニメに合わせてKindle版の1,2巻が値下げされています)

ダンジョンに出会いを求めるのは間違っているだろうか (GA文庫)

ダンジョンに出会いを求めるのは間違っているだろうか (GA文庫)

前に小説家になろうのデータでいくつか記事を書きましたが、今回はpixiv 小説の小説で男性向けか女性向けかの分類を試してみますsucrose.hatenablog.com
sucrose.hatenablog.com

pixivの小説ランキングは男性向けを探すのは難しいのが既知の問題として知られています[要出典]
一応男女別人気ランキングとして「男子に人気」「女子に人気」のランキングがありますが、分け方がてきとーです
男子に人気ランキングを見ればわかりますが男子に人気のトップ10がすべて刀剣乱舞なのでたぶんだいたい腐向けです
ちなみにトップ50のうち刀剣乱舞以外のものは7件しかありませんでした

ランキングの小説が男性向けかどうかを自動で判定できないか、機械学習を使って簡単に試して遊んでみました

なお「男性向け」「女性向け」という言葉をゆるふわに使ってますがご容赦ください

データの収集

「男子に人気」「女子に人気」のランキングを、それぞれ男女のラベル付きの教師データとみなして利用します
なぜか過去のランキングはないのでデータは男女100件ずつしかありません(イラストの方には過去ランキングがあるのに

分類器

線形SVM(L1正則化)を使用

特徴量

小説の情報のうち使えるのは、本文、タイトル、タグ、キャプション、作者などいろいろとあります。
今回は本文とタグのそれぞれの頻度をTFIDFで重み付けしてベクトルとして学習してみました

結果

残念なことに(?)本文を使うよりもタグを使ったほうが性能がよかったです(メタデータはコンテンツそのものの情報を使うよりも分類に役立つことがよくあります)

タグ

小説ごとにどのタグが含まれていたかのベクトルを作ってSVMで学習しました
データが少なかったのと、めんどくさかったのでクローズドな評価です(訓練データそのものを分類して評価する)
「女子に人気」のランキングの小説は9割ぐらいが正しく判定できていましたが、「男子に人気」ランキングの小説は5,6割ぐらいしか正解できませんでした
「男子に人気」ランキングの分類が多く失敗しているのは、「男子に人気」ランキングにも女性向け作品が多いという観察結果と合致しています。
この結果はまあまあ女性向け作品を分けることができていると考えるべきでしょう

分類への影響が大きかった、重みの絶対値の大きなタグを確認すると特徴的な作品やキャラのタグをちゃんと学習できているように見えます(というか刀剣乱舞強すぎです!)

男性向け 女性向け
やはり俺の青春ラブコメはまちがっている 刀剣乱腐
ラブライブ 刀剣乱舞小説100users入り
比企谷八幡 刀剣乱腐小説100users入り
naruto 腐向け
サスサク 審神者
ナルヒナ ブラック本丸
雪ノ下雪乃 刀剣乱夢
東條希 刀剣乱舞
艦これ とうらぶちゃんねる
naruto小説50users入り 混合小説100users入り

ちなみにタグの文字n-gramを使って学習したら「100users」の部分文字列や「腐」などの重みの絶対値が大きくておもしろかったです
pixivの小説は女性ユーザーが多いので、上の重みの絶対値の大きなタグみてもわかるように「100users」などの人数が大きなタグがついているだけで女性の確率が高くなるという現象があります
教師データが意図した通りの基準で分けられているかってのは難しい問題です
男女で分けたかったのに、人気の大小の判定機ができたりするかもしれません

本文

小説の本文を使った場合も試してみました
単純に本文を形態素解析して形態素の頻度のベクトルを使いました
訓練データが少ないせいか文章が短いせいかあまり精度がよくなかったです(手法もナイーブ過ぎた

主にキャラ名(の一部)などが重みの絶対値の大きな単語として出てきますが、他にも男性向けだと「ちゃん」「かしら」「ましょ」などの女性を表していそうな単語が出てきて、女性向けだと「お前」「だろ」「きみ」「ボク」などの男性っぽい言葉遣いが出てきます

まとめ

そこそこ男性向けと女性向けに分類できました(作品で判別しているだけなのでオリジナルでは難しそう
本当はセリフ内かどうかを考慮したり長さとか品詞ごとの頻度とかを入れたり特徴ベクトルを工夫したほうがよいと思います

男子に人気の分類結果が女子向けと判定されるのが多めだったので自分の人力で男子に人気ランキングを分類してみました
刀剣乱舞などを除くと100作品中4,50件ぐらいしか男性向けと思われるものはありませんでした

結論としては、男性向けのWeb小説を読みたい人はpixivだと難しいので、オリジナルなら小説家になろうや二次創作ならハーメルンで読みましょう()

おまけ - 各小説サイトごとのイメージ

以下主観で特徴を列挙しました

小説家になろう
最大の(?)Web小説サイトで基本オリジナル系。ファンタジー、チート、成り上がり、ハーレム、勘違い系などいろいろと独特の特徴があります。
pixiv 小説
イラスト投稿サイトとして有名なpixivの小説版。主に女性ユーザーが多くランキングは腐女子向けの二次創作小説がほとんど。
ハーメルン
二次創作小説投稿サイト。主に男性向け(?)
Arcadia SS投稿掲示板
昔からあるサイトです。オリジナルと二次創作どっちもありますが、最近は投稿数が少なめ

ソースコード

タグで適当に分類する版(sklearnとかpyqueryなどのライブラリを使っています)

# -*- coding: utf-8 -*-
import re
import time
from pyquery import PyQuery
import codecs
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.svm import LinearSVC
from sklearn.cross_validation import cross_val_score
from sklearn.metrics import confusion_matrix
from sklearn.metrics import classification_report
import numpy as np

def read_tag(url):
    pattern_id = re.compile(r'id=(\d+)')
    q = PyQuery(url)
    tags = []
    for elem in q.find('.ranking-item'):
        tags.append(PyQuery(elem).find('.tags a[class!=tag-icon]').text())
    time.sleep(1)
    return tags



pattern_id = re.compile(r'id=(\d+)')

#データの取得
female_tag = []
female_tag +=  read_tag('http://www.pixiv.net/novel/ranking.php?mode=female&p=1')
female_tag +=  read_tag('http://www.pixiv.net/novel/ranking.php?mode=female&p=2')

male_tag = []
male_tag +=  read_tag('http://www.pixiv.net/novel/ranking.php?mode=male&p=1')
male_tag +=  read_tag('http://www.pixiv.net/novel/ranking.php?mode=male&p=2')

#特徴ベクトル化
vectorizer = CountVectorizer(min_df=3, ngram_range=(1, 1))
data = vectorizer.fit_transform(female_tag + male_tag)
tfidf = TfidfTransformer()
data = tfidf.fit_transform(data)

print u'パラメータを変えながら適当にクロスバリデーション(精度)'
for i in xrange(-10, 6):
    model = LinearSVC(C = 4**i, loss='l1')
    scores = cross_val_score(model, data, [1] * len(female_tag) + [-1] * len(male_tag), cv = 20)
    print i, scores.mean()

model = LinearSVC(C = 4**(-5), loss='l1')
model.fit(data, [1] * len(female_tag) + [-1] * len(male_tag))

print u'男性向け女性向けそれぞれの重みの大きな特徴量'
print ', '.join(np.array(vectorizer.get_feature_names())[np.argsort(model.coef_[0])[:10]])
print ', '.join(np.array(vectorizer.get_feature_names())[np.argsort(model.coef_[0])[-10:][::-1]])
print
print 'confusion_matrix'
print confusion_matrix([1] * len(female_tag) + [-1] * len(male_tag), model.predict(data))
print 'classification_report'
print classification_report([1] * len(female_tag) + [-1] * len(male_tag), model.predict(data))
print
print u'訓練データ自体を予測した結果'
print model.predict(data)
print u'ルーキーランキングを予測した結果'
print model.decision_function(vectorizer.transform(read_tag('http://www.pixiv.net/novel/ranking.php?mode=rookie')))