Chienomi

DLsite Voice UtilsにStatを追加

開発::util

この記事は題材としてアダルトコンテンツに関わる内容を多分に含む。

DLsite Voice Utilsは、DLsiteの音声作品コレクションを整理・閲覧するためのものである。

このプログラムは随時更新されているのだが、リポジトリ上で更新しているわけではないため、リポジトリは放置されている状態だった。

今回、新しいプログラムとしてVoice Statを追加したのにあわせてリポジトリを更新した。

そして、Voice Statは実験的なものとして、ユーザーの好みを分析する機能を搭載している。

Voice Statの概要

Voice Statは以下の内容を出力する

  • actress別の作品数
  • circle別の作品数
  • tags別の作品数
  • actress別の平均スコア
  • circle別の平均スコア
  • score == 5の作品
  • actress別のscore == 5の作品数と比率
  • circle別のscore == 5の作品数と比率
  • 総作品数
  • 平均スコア
  • 平均作品時間
  • レートをつけていない作品の数と率

こんな感じで表示される。

========== TOP CAST ==========
陽向葵ゅか: 48 [7]
秋野かえで: 32 [4]
涼花みなせ: 32 [6]
柚木つばめ: 25 [6]
こやまはる: 24 [13]
...

...

========== 5 Stars Actress ==========
逢坂成美 5/9 (55%)
柚木つばめ 4/6 (66%)
こやまはる 4/13 (30%)
涼花みなせ 3/6 (50%)
陽向葵ゅか 3/7 (42%)
...

加えて実験的な機能として、好んでいる出演者とサークルを分析し、スコア順に表示する。

========== Maybe you like ==========
こやまはる
逢坂成美
涼花みなせ
柚木つばめ
陽向葵ゅか
...

「好みの分析」

前提

最近の一般的な指標であれば、単純に作品数が多い=好みであるとみなすだろうが、自分でやっているものだけに正しさの度合いは明白である。 異なる集計方法が必要であることもまた、明白なのだ。

本ソフトウェアの場合、作品数をベースに普通にやると、絶対に陽向葵ゅかさんが一番スコアが高くなる。 なにせ出演作品数が圧倒的なので、よっぽど強いバイアスのもとに購入していない限り、陽向葵ゅかさんからは逃れられない運命にある。

なお前提として、集計対象がスコアをつけたものだけになるため、所有作品全体に対してスコアをつけたものに偏りがあると違和感のある結果になりやすい。

さて、ユーザーから与えられている情報と分析しようとしている情報を整理しよう。

音声作品には基本的な構成要素として

  • サークル
  • シリーズ
  • 出演者
  • ライター
  • イラストレーター

がある。

この中でVoice Utilsでユーザーから与えられる情報はサークル、シリーズ、出演者である。

加えてユーザーから提供される情報は

  • タグ
  • 作品の長さ
  • スコア

である。 他にもdescription, noteという項目があるが、これは自由項目なので分析には使えない。

この他に日付情報がある。 これは複雑で、まずユーザーが編集可能なデータである。

そして、自動で埋まるデータでもあるが、これはbtimeをいれるようになっている。 この日付の意図は基本的には「購入日」であるが、それは購入したらすぐにライブラリに追加・展開したという事実が必要だ。

また、Ext4のようなbtimeを使えないファイルシステムではmtimeを使うが、アーカイブから展開する関係上、RAR形式でアーカイブにmtimeが入っていたりすると、販売側がそのファイルをアーカイブした時点のファイルのmtimeになってしまうため、購入日として使えない。

こうした理由からかなり不安定な値である。 一応、意図は購入日であり、最近買った作品を見つけやすくするためのものだ。

ユーザーが好むかどうかを判定する上で重要となるライターの情報が入力されていない、というのはかなり大きい。 また、タグ情報はユーザーが作品そのものを肯定的に捉えているか否定的に捉えているかを知る手がかりになるが、タグ自体は肯定的・否定的の属性を持っていないため、スコアから判定するしかない。

ウェイトつきの掛け合わせ

これを書いている時点でのコードは次のようなものだ。

calc_favorite_actress = []
dist_rate = Hash.new(0)
dist_rate_vs = STAT[].values.flatten
dist_rate_vs.each {|i| dist_rate[i] += 1}
sc_base = {
  1 => 0.75,
  2 => 0.9,
  3 => 1.0,
  4 => 1.08,
  5 => 1.45,
}
sc_deviation = {
  1 => -0.14,
  2 => -0.11,
  3 => -0.05,
  4 => 0.04,
  5 => 0.21,
}
sc = {}
dist_rate.each do |k,v|
  r =  v / dist_rate_vs.length.to_f
  sc[k] = sc_base[k] + sc_deviation[k] * (1 - r)
end

STAT[].each do |k, v|
  next if v.length < (OPTS[:"recommend-works-limit"] || 4)
  # Base score
  score = v.length + v.length * ((v.sum(0.0) / v.length) / (STAT[].sum(0.0) / STAT[].length))

  wt = Hash.new(1)

  v.each do |i|
    case i
    when 5
      score *= (sc[5] + wt[5] * 0.23)
      wt[5] += 1
    when 4
      score *= (sc[4] + wt[4] * 0.12)
      wt[4] += 1
    when 3
      wtr = (sc[3] - wt[3] * 0.01)
      wtr = 0 if (wtr < 0)
      score *= wtr
      wt[3] += 1
    when 2
      base_rate = case
      when v.include?(5)
        sc[2] + 0.12
      when v.include?(4)
        sc[2] + 0.08
      else
        sc[2]
      end
      wtr = (base_rate - wt[2] * 0.02)
      wtr = 0 if (wtr < 0)
      score *= wtr
      wt[2] += 1
    when 1
      base_rate = case
      when v.include?(5)
        sc[1] + 0.3
      when v.include?(4)
        sc[1] + 0.21
      else
        sc[1]
      end
      wtr = (base_rate - wt[1] * 0.03)
      wtr = 0 if (wtr < 0)
      score *= wtr
      wt[1] += 1
    end
  end

  score *= (v.sum(0.0) / v.length / 5)

  calc_favorite_actress.push({k, score, v.length, (STAT[][k].sum(0.0) / STAT[][k].length)})
end
calc_favorite_actress.sort_by! {|i| -i[]}

つまり、キャストに対するものとサークルに対するものそれぞれに対して、つけられたスコアにはウェイトがある。 作品数に基づいて算出されたベーススコアに当該項目に対してつけられたスコアに基づいたウェイトをもつレートを掛け算していくことにより、評価を決定する。

掛け算式ウェイトにはいくつかのポイントがある。

まず、このウェイトは完全に掛け算であり、よって処理順序に依存しない。

ベーススコアは作品数 + 作品数 * 平均レートであるが、これも単純な掛け算になった場合はベーススコアの値そのものにも意味がなくなる。

まず、仮にレートが[4, 3, 3][5, 3, 2]のものがあったとする。 平均はどちらも3.33であり、単純にレートをかける方式だとこれらは等価である。

対してdeviationがないと仮定すると、ウェイトつきのものは1.081.305で差が生じる。

この差をどのようにつけるかが結構重要だ。

まず、ベーススコアは作品数と平均レートによって決定される。 平均レートによってベーススコアが下がることはないため、作品数が多ければ多いだけベーススコアは増加する。

ウェイトが小さいと、ベーススコアからの変動幅は小さくなるので、作品数の影響が大きくなる。

一方、ウェイトが大きいと、低いレートの作品が追加されたときにスコアが大きく下がったり、高いレートの作品が存在することで少ない作品数でもスコアが大きく増加したりする。

重要なのはバランスだ。 出演者およびサークルは購入判断にも絡んでいるはずなので、作品数が多いという特徴を持っているということは肯定的に捉えても良いはずだ。

ただ、単純にリリースされている作品数が多いという傾向の問題もあるため、単純にはそう考えられない。 また、一定数の作品を購入したが、あまり好まず遠ざかったという可能性もあるから、現時点で好んでいると断定できるようなものでもない。

要素の主従

ソフトウェアとして展開することを考えると、それがどのような要素であれ当てはめられるものであれば一定の式で計算する、というのが望ましい。

だが、本ソフトウェアは「個別性は非常に重要である」ということを示している。

本ソフトウェアで評価の中心となっているのは出演者とサークルである。

サークル自体を好む好まないというのは難しい要素であり、なくはないことではある。 例えば、サークルの過去の行いから嫌うようになった、などだ。 しかし基本的には、サークルの評価というのはサークルに作品傾向があるということを前提にしている。 これは、サークルに起用傾向があったり、サークルのスタッフの性質が影響していたりといったことだ。 サークルの中身が完全に変わってしまうということはなかなかないので、サークルに対して「合う合わない」というのはユーザーはある程度以上持っているものと考えていい。 より一般的な視点(例えば映画や音楽など)で言うと、サークルの名義は監督、あるいはプロデューサーのようなものだ。

だが、本質はサークルではなく作品である。 サークルの情報は、サークルでくくることで作品傾向がある程度グループ化できるということを前提にしているに過ぎない。 だから、複数ライン持っているようなサークルだとややこしいことになる可能性がある。

よってサークルという要素は実際にはユーザー評価に対する決定的なものを持っているわけではなく、これを「作品」で見る必要がある。

さて、「作品は嫌いな内容だが、出演者は好きである」と「作品は好きな内容であるが、出演者は好みでない」は等価だろうか?

もちろんこれはNOだ。 音声作品の場合は作品の内容がダメであれば、出演者がどんなに好みでもどうにもならない。 出演者が作品評価に大きな影響を与えているのは間違いないが、どう考えても作品がベーススコアで出演者は係数になるもの、だから作品が主で出演者は従だ。

これは、音声作品というものの性質ありきである。 例えばAVで考えてみれば、よっぽど受け入れられないタイプの作品でなければ出演者が好きなら一定の評価を与えられるだろう。 許容できない作品である場合はともかく、駄作であっても出演者の力である程度のスコアを出せる。 これは、AVというものの性質上、出演者の裸体だけである程度のコンテンツ価値を生成できるからだ。

ところが、音声作品だとコンテンツの幅がもっと広いため、本当に駄作だと出演者の努力も虚しく駄作にしかならない可能性が高い。 つまりは台本の影響力の差だ。

それを踏まえて、ある評価が[5, 5, 3, 1, 1]だったとする。

これが出演者の評価であった場合、1があることは出演者を好んでいない、という判定はできないだろう。 5があるということは、その出演者の作品の中に傑作があるとみなしているわけだ。 その出演者の作品は安定性が低い、つまりは出演者の指名買いは失敗する可能性が高いと考えることはできるだろうが、その出演者の作品を避けるべきだとは考えられない。

対してサークル評価であった場合、1がつけられたという事実は重い。 サークルの作品を買って立て続けに1をつける状態であった場合、もうそのサークルの作品は買わない可能性がかなりある。 対して5が出たことに対しては、起用した作家の問題であったり、あるいはたまたま筆が乗ったという可能性もあり、過剰に高くは評価できない。

よって、出演者とサークルは同じ尺度でスコアを判断することができないのだ。

しかも、これはあくまで私の感覚に過ぎない。 中にはどんな駄作でもこの人の声が聴ければ幸せ、という熱心なファンもいるだろうし、ひとたび傑作を出したサークルならば駄作を連発しても期待し続けるという人もいるだろう。

その人の購買モチベーションがどうなっているか、ということを判定できないと、何に対するスコアがどれくらいのウェイトを持っているかを判定できないのだ。

ウェイトの配分

私が使う分には、スコアをどのようにつけているかはわかりきったことであるので、スコアに対して妥当なウェイト配分は判断できる。 しかしこれを汎化させるとなるとそうはいかない。

私は良かったなら4、凡庸なら3、おそらくもう聴かないであろうものは2をつけている。 5は傑作であるという判定であり、1をつけるのは二度と開かないほうが良いいわゆる「地雷」だ。

しかしそうではない基準の人は当然いるわけで、ジャスト好みでなければ全部1をつける、という人もいるだろうし、45しか使わない、という人もいるだろう。

そこで実装されているのがsc_deviationだ。

これは、その評価をつけられることがどれだけ稀であるか、というものであり、コード上で見れば稀であるほどウェイトを重くする。 だが実際は、「頻繁につけられている評価のウェイトを下げて1.0に近づける」という意味である。

sc_baseは最低限この値を掛けるということを保証しているものになる。

現在は単純な頻度で算出しているが、ゆくゆくは偏差から算出することを考えている。

現時点ではスコアは「ウェイトをかけたスコア」という扱いだが、ユーザーのスコアを均等でない形に割り振ることができれば、偏差値から評価を決定できる。

対応スコアは分布から算出するようにすると良いだろう。 1ばかりつける人の1はそこまで低いスコアではないから60くらいあるかもしれないし、1をほぼつけない人の10でいいかもしれない。

こうして対応スコアを出すと、偏差を使って分析できるようになる。 今よりもだいぶすっきりするだろうし、スコアが安定しているか不安定かというあたりも分析できるようになるだろう。

ということが分かっているのになぜそうしていないか。 理由は簡単、適切な式を思いついていないからだ。 数学ができない自分が憎い。

複合要因

現時点では、出演者なら出演者の、サークルならサークルの作品ごとのレートをもとに算出しているが、実際はもっと複合的だと考えたほうが良い。

出演者のスコアリングで考えてみよう。

まず、サークルは起用するキャストにある程度の偏りがあるものであり、これによってサークルの評価が出演者の評価に大きな影響を与える場合がある。

また、キャラ付けという問題があるため、特定のシリーズにおけるこの人が演じるキャラクターが好き、というパターンもあり、必ずしもその人の出演作全般が好きとは限らない。

さらに、作風によって合う合わないの問題から好みが割れてしまうこともある。 例えば、お姉さん役が多い人で、その人のお姉さん役は好きだが、子供っぽい役は好きではない、などだ。

前述のようにユーザーから与えられている情報は限定的であるため、すべてを正しく推測できるわけではなく、そもそもこの問題の解決は困難である。

例えばサークルの影響を下げる方法はある。 「出演者+サークル+スコア」の組み合わせが重複する場合、ウェイトを軽くするといった方法だ。 順序に関係なくこれをかけるためには、あらかじめウェイトを軽くした上で計算する必要がある。

だが、これは本当にいいのだろうか。

例えば、「つばめいとの柚木つばめさんが好きで、柚木つばめさんの作品全体的に好むようになった」みたいなケースは全然あるわけだ。 となると必然的に「柚木つばめ+つばめいと+5」という組み合わせは多くなるわけだが、それじゃあこの組み合わせのウェイトを下げて柚木つばめさんの評価が高くなりすぎないようにしましょう、というのが正しいのかはかなり疑問。

単に作品の性質と出演作の傾向の問題である可能性もある。 これは、タグからある程度判断できる。 タグの数と平均レートから、「好んでいるタグ」の判断は割とつけやすい。 好んでいるタグと出演者の重複が大きい場合、出演者の評価はタグの影響を受けていると考えられる。

だが、これはあくまでも「考えられる」というだけで、適切に影響を除去し、出演者というファクターだけを取り出すのはやはり困難である。

現時点では私のデータでレートをつけたのが100件に到達しておらず、5件以上評価している出演者もサークルも一握りであるため、そもそもウェイトの計算式ですら検証できていないという理由から複合要因を計算する処理はまだ入っていない。

変動

特にサークルの場合、「変動」という要因も無視はできない。

実際、私は「そのサークルをフォローして新作を買うようにしていたが、今ではフォローを外してそのサークルの作品を避けている」というものもある。

理由は、気に入った作品があったから期待値が高かったが、いまいちな作品が続いて期待値を消費しきってしまった、という場合もあるし、サークルが変化したことで好まなくなったという場合もある。

サークルの評価が下がって結果買わなくなった、という場合、与えられた情報をもとにするとそのサークルは評価が高い状態になってしまう。 例えば[5, 5, 5, 5, 4, 4, 1, 1]だとすると、このサークルの評価はかなり高い。 が、実際はそのサークルをブロックしている可能性すらある。

少なくとも「サークルに対して低い評価をつけた状態で途絶している」ということは判定したほうが良い。 だが、その適切な判定方法はかなり難しい。

好みの分析に「後のものほどウェイトを重くする」という方法もあるが、直近にたまたま駄作が続いたというケースでもそれを回復するのが難しくなる。

途絶期間を設定することもできるが、リリースペースと購入ペースというものもあるので難しい。

また、過去作品を購入することも可能なので、現在の日付方式だとここらへんの判定はより難しい。

現状

「分析の精度はデータ量を要求する」を痛感させる現状である。

だが、得られる知見は現状でも多い。 メディア特性や要素の主従があるためにすべてをひとつの式にまとめるのは無理があるということや、ユーザーの感覚や習性を分析に反映させることの重要性などは特に意味が大きいだろう。

私が入力しているデータ量が足りないこともあり、まだ端緒に着いたばかりだ。

読みがな機能

その他の大きな更新として、actressとcircleに読みがなをつけられるようになった。 これは、ページでの項目のソートに使われる。

actressの場合はファイル名を[読み]_表記とすることで、circleの場合は_[読み]_表記とすることでsoundindex.jsというファイルに組み込まれる。