にほんごのれんしゅう

日本語として伝えるための訓練を兼ねたテクログ

bertで知る炎上とブランドイメージの関係

bertで知る炎上とブランドイメージの関係

イントロダクション

近年のSNSでの炎上は企業にとって大きなリスクとして認識されています。炎上してしまうと、企業はその対応に追われ、多大な労力を払うことになります。また、企業のブランドイメージの既存があると一般的に認識されているようです。
2020年は企業・国務に関連した多くの不祥事がありました。不祥事が起こるたびにその対策は行われてきましたが、炎上自体が引き起こす、ブランドイメージの低下等は定量化されていないようです。
今回、twitterのデータと機械学習のbertと呼ばれるアルゴリズムを用いることで、炎上した企業・商品・公人がどのような影響を受けたかを定量化し、曖昧であった炎上のリスクを可視化したいと思います。

類似した研究等

どのように定量化したか

twitterのデータを取得し、bertの感情分類モデルでスコア付けし、統計処理を行うという流れになります。

データの取得
twitterのデータはtwintと呼ばれるapi likeに使えるプログラムを用いて取得しました。
全て取得すると非常に時間がかかるので、一日あたり1000件に限定して取得を行いました。

twintの使い方を私のgithub.ioでまとめているので必要に応じて参照してください。

分析対象の選定
2019 ~ 2020年度の炎上のまとめサイトを見ながら、対象とするキーワードを選択しました。

選択したキーワード

ブランドイメージを定量化する機械学習
twitterに出現するような表現に対して柔軟かつ精度良く、そのツイートが良い感情をもって表現されたかネガティブな感情をもって表現されたかを推論するアルゴリズムに、bertと呼ばれるアルゴリズムを用いました。
bertpretrained modeltweetを入力すると、ポジティブネガティブのフラグと0.0 ~ 1.0のスコアが得られます。これをネガティブを負の値に変換して1/2にスケーリングして、ポジティブを1/2にスケーリングすると-0.5から0.5に正規化された値を得ることができます。

このスコアを利用して、あるキーワードに対する一日あたりの平均のスコアを算出することが可能になります。平均したスコアを28日(4週間)の移動平均をとり、乱雑さを軽減しました。
x軸日付, y軸平均スコアが表示されることになり、炎上を経験した企業・商品・公人のイメージを追跡することが可能になります。

実際にこのモデルにてプリキュアを分類した結果を確認すると以下のようになり、期待した動作が得られました。

スクリーンショット 2021-02-06 12 40 17 スクリーンショット 2021-02-06 12 40 34

上:ポジティブな検出例, 下: ネガティブな検出例

具体的な機械学習でのセンチメント(感情)分析の方法はこちらを御覧ください

統計を正確にするためのヒューリスティクス
twitterのデータを扱う際には気をつけるべきポイントが幾つかあります。広告目的やポイント等のインセンティブを狙った機械的な投稿や、偏執的に同一の話題に対して何度も発言する人などのバイアスを外す必要があります。

そこで以下のようなルールを入れました。

  • ある人が一日に特定の話題について複数回話していても、採用するのは一回のみ
  • http等URLを記入した投稿は採用しない

結果

東京五輪

大規模なネガティブなイベントを捉えることに成功しています

直近の感染者増加、森元首相の発言等で更に東京五輪のイメージは冷え込みそうです。
Twitterのデータから取得したのでTwitterユーザの特性が反映されるというバイアスを考慮した上でも、東京五輪の印象は右肩下がりのトレンドを持っているようです。

けものフレンズ

テラスハウス

自殺という非常に重い話題のためか、テラスハウスの印象の回復に相当の時間がかかっているように見受けられます。

くら寿司

くら寿司バイトテロに対して訴訟を起こし業界の健全化を図ろうとするなど、社会的に良い行いを行おうとしています。
テラスハウスが回復が遅いのに対して、すぐにイメージの回復を行えている等、炎上時の初期対応がいかに重要かがわかります。

ドコモ口座問題

この問題の本質は銀行との認証のやり取りに問題があるということでしたが、銀行側のイメージはサービスに比べて、すぐに回復しているようです。
ドコモ口座の印象は地に落ちた状態になっており、ここから回復するのは至難に見えますが、ドコモさんはサービスを継続するようです

電通

わたしが新卒の頃は電通さんは高給取りでそんなに悪いイメージはなかったのですが、SNS上では年々、電通さんに対してネガティブな投稿が増えているように感じました。
長期的なトレンドを把握したく例外的に長期間集計したのですが、高橋まつりさんの事件をきっかけに、細かいイメージダウンを伴う炎上を繰り返して、イメージが下降傾向になっています。電通報を一時期鍵垢にするなど説明を果たす立場をとり、炎上が静まるまで待ってしまった戦略が尾を引いているのではないかと想像してしまいました。

各政党

NHKの調査によると、最も支持されているのは自民党であるということになります。
政党に対する感情と、政党支持率は全く関係していないことが明らかになりました。

現政権を担う政党であるから最も批難される立場にあるということを考慮しても、支持率と感情が比例していないのは驚きの結果であります。

コンビニ各社

規模が大きなコンビニでは、セブンが一人抜きん出てイメージが強いですが、セブンペイの不祥事等でイメージが一時的に低下しています。ミニストップセイコーマートのような規模がそんなに大きくないコンビニはイメージがよいです。

セイコーマートが顧客満足度で一位というデータも有ます。

議論

2020年あたりに発生した主要な炎上事件を見ていきました。

わかったこととして、炎上が発生するとイメージが低下すること、時間が経つことに多くは回復するが、くら寿司やテスラハウスを見るとその後の誠意のある対応を行ったかどうか等で回復のスピードが異なるなど、対応の方法が炎上後の回復に影響していそうなことがわかりました。

政治に関し、実際の政党支持率と感情に関係がない結果になりました。小さい政党ほど熱心なファンを獲得しやすいという傾向があるように見えます。この傾向はコンビニでも同様でした。
コンビニの各社に対する感情も、売上と感情は比例していません。

結論

企業のイメージと売上との関連を見たいと考えていたのですが、電通さん以外は、説明するのが難しいと考えました。SNSの影響は年々強いものになっていますが、今現在はSNSに影響されない人も相当数存在する社会であり、企業のSNSの炎上のダメージが及ぼす売上への影響はまだ支配的でないと考えます。

しかし、SNSの使用時間は年々増加しており、SNSから受ける印象が企業の売上に影響を与えるということがありえてくるでしょう。

かんたんな機械学習と統計操作で感情の分析をすることも可能であり、リアルタイムに推論と集計を行うことで炎上検知等にも用いることができそうです。

いずれにせよコンプライアンスを遵守し、SDGs等で企業イメージを一定以上に保つというのは、これからの情報化社会を考えると、やるべきことだと言えます。

コード管理とデータセットについて

botはツイートを圧縮するとサイズが小さくなることを利用して、botの検出

今やっていること

真面目に相性を考慮した企業推薦アプリやマッチングアプリを作りたい
企業への就職や出会いを求める場など、現在はITが進んでいますが、まだ最適な状態に至っていいないだろうと思われます。そんな課題を解決するために、人の行動ログ(ここではSNSでの発信ログ等)を利用して、真面目なマッチングエンジンを作ろうとしていました。

具体的な多くの人の行動ログを取得可能なサービスを所有していないので、Twitter社のデータを用いて、マッチングエンジンを作ろうとして現在、技術検証や精度の改善などをしています。

日本語のテキストを書くユーザ 2400万人 分の直近500 ~ 1000ツイート程度をサンプリングしており、さまざまな観点を検証しています。

安西先生...、botが邪魔です...!
狭い課題として、botと呼ばれるプログラムでの自動運用されたアカウントが少なくない数存在し、botは特定のキーワードを何度もつぶやくので、個性や特性を重要視したマッチングエンジンを作成した際に悪影響を及ぼす可能性が強くあります。

そのため、狭いスコープの課題ですが、botをデータと機械学習でうまく検出し、ブロックする仕組みを構築しましたので、ご参考にしていただけますと幸いです。

先行研究

奈良先端科学技術大学院大学が数年前に出した軽めの(?)論文に、認知症者􏰀発言􏰁圧縮すると小さなサイズになるというものがあります。

認知症の人は、認知症ではない人に比べて、発話した情報が、圧縮アルゴリズムにて圧縮すると、小さくなるというものでした。

ハフマン符号化をかけることで、頻出するパターンをより短い符号に置き換えでデータの圧縮サイズをあげようというものになります。

pythonで処理するには同系統アルゴリズムであるbzip2が便利

bzip2は、明確に ブロックソート法ハフマン符号化 を行っており、かつ、zipという圧縮方式より結果が綺麗に出たので、採用しました。

具体的には、以下のようなプロセスで元のテキストファイルサイズと、圧縮済みのファイルサイズを比較します。

import bz2
import os
import random
from pathlib import Path

salt = f"{random.random():0.12f}"
pid = f"{os.getpid()}_{salt}"

all_text # ある特定のユーザのツイートを1つにjoinしたもの

# そのままのテキスト情報を書き込んだときのサイズを取得
with open(f"/tmp/{pid}", "w") as fp:
   fp.write(all_text)
original_size = Path(f"/tmp/{pid}").stat().st_size

# bz2で圧縮した時のサイズを取得
with bz2.open(f"/tmp/{pid}", "wt") as fp:
    fp.write(all_text)
bz2_size = Path(f"/tmp/{pid}").stat().st_size

# clean up
Path(f"/tmp/{pid}").unlink()

圧縮アルゴリズムで作った特徴量と、特徴量エンジニアリングで作った特徴量を組み合わせて学習

ラベルの定義

ユーザネームの末尾が _bot になっているものをbotと定義

botはかなりの数存在し、検索を汚染するような形でよく出現します。twitterのユーザーネームで末尾に_botをつけているアカウントが存在し、これはほぼbotだろうという前提で処理して良いものであろうと理解できます。ユーザーネームの末尾が _bot ならば、botと定義しました。

縦軸に 圧縮したファイルサイズ/オリジナルファイルサイズ , 横軸に 機械的な周期性 を取ると、上記のような散布図を得ることができます。

ノイズもままある
_bot と末尾のユーザーネームにつけているのにbotでない人、それなりにいるんです。Twitterをやっていると理解できる事柄ですが、自身のアイデンティティなどを機械などと近い存在であると思っている人は _bot とかつけることがあるようです。
手動でクリーニング仕切れない量程度には、人なのに、 _botサフィックスをつけている人がいるのですが、機械学習でうまくやることで解決していきます

例えば、上記の図の左側でまるで囲まれた _bot であると自称しているユーザは、実際は典型的な人間のツイートをしており、botではありません。

_bot が末尾のものだけでは、precision によりすぎている

真の課題は、_botサフィックス等がつかなくても人間のユーザのフリをするbotアカウントを検出することであります。このたぐいのアカウントは大量にあり、自動運用されている宣伝・企業アカウントなどはまずbotであります。

そのため、 _botサフィックスでの判定は例外があるものの、機械学習によるprecisionによりすぎた判別をrecall側に倒す作業とも捉えることが可能になります。

モデルの閾値を調整し、現実的なリコールになるようにする

特徴量を選定する

今回のスコープとしてrecallを上がるように倒したいので、モデルの複雑度を上げすぎないことと、ノイズとなるbotでないのにbotを自称している人を判別してはいけないので、うまく丸めるため特徴量を多くしすぎないことがポイントとなります。

以下の特徴量を選択しました。

  • compression_ratio: 圧縮率
  • is_near_rate: 前のツイートから10分の倍率でツイートした率
  • uniq_ratio: ユニークツイート数

モデルの作成

LightGBMで、特徴量を3つ用いて、構築する木の複雑さを3に抑えて、AUCをメトリックとして学習を行いました。 Holdoutで25%をvalidationとして、AUCが悪化しない範囲で学習を継続します。
特徴量単体だと、AUCが 0.90程度までしかでませんが、 0.922 まで上げることが可能になりました。

作成したモデルのしきい値探索

どの程度、botと判定していいのかを定義する

_bot がつくアカウントがTwitterのアカウントの全体の 0.05% 程度でした。定性的な感想ですが、2%程度は完全にbotに近いアカウントであり、学習したLightGBMのモデルの閾値を0.05まで緩めることで、2%程度のリコールを得ることができました。

0.05のしきい値で推論した結果

素の _bot だけの散布図より、オレンジの面積が大きく広がっていることがわかります。

モデルの結果の定性評価

リコールを広げた結果、具体的にどのようなアカウントがボットと判定されたのか見ていきます。

著作権法32条に基づき、技術検証のため、公開情報を引用しています)

例1: もう使用していないアカウントでアプリ登録をしたアカウントをボットとして判定

連携したアプリからの宣伝のみなのでbot判定でOK

例2: 出会い系の誘導の業者アカウントをボットとして判定

実態は偽装した業者アカウントbotなので見抜けており、期待した挙動である

Twitter APIでまとめてbotをブロックする

このbotの検出は価値がある作業で、Twitterの検索結果をbotや業者が激しく汚染する、という経験を体験している方は多いかと存じます。
検出されたbotTwitter APIとそれをpythonで簡単に使えるようにしたtweepyをインストールすることで、簡単に特定のユーザをblockすることができます。

書捨てのコードだと以下のようにして実行することができます。

入力となるcsvデータは末尾のDropboxのリンクに付属します。

import time
import tweepy
import os
import pandas as pd
from tqdm import tqdm

auth = tweepy.OAuthHandler(os.environ["API_KEY"], os.environ["API_SECRET_KEY"])
auth.set_access_token(os.environ["ACCESS_TOKEN"], os.environ["ACCESS_TOKEN_SECRET"])

api = tweepy.API(auth)

df = pd.read_csv("./tmp/result.csv")
df.sort_values(by=["yhat"], ascending=False, inplace=True)

for username, yhat in tqdm(zip(df.username, df.yhat), desc="blocking...", total=len(df)):
    try:
        api.create_block(username)
        # time.sleep(0.1) # you may need this
    except Exception as exc:
        print(exc)
        continue

データとコード

  • GitHub: 再現をしたい場合、何をやったかが確認できます
  • Dropbox: 最終的な、推論したスコアを付与したデータ
  • Dropbox: Githubにあるコードを再現するためのデータ(オリジナルのTweet情報は含みません)

Webアプリにできないでしょうか?

  • 常に新鮮なコーパスはあり、集計はできる
  • TwitterIDでログインすると、一括でブロックできるアプリは作ることができ、また、世の中に必要な気がします。

技術書典8の製作物を無料公開します

お久しぶりです。 私生活で反省することが多く、心身ともに疲弊し、しばらくTwitterやブログなど対外的なアウトプットをお休みしていました。

技術書典8で復活を遂げようと思っていたのですが、コロナのために開催がなくなってしまいました。

いろいろな人を頼り、助けてもらい、多くの人の力で作ったコンテンツであったので、死蔵させいても仕方がないと思い、公開します。

サークル名は Practical Data Science and Data Engineering で、いかにも、、、な名前にしました。あまりひねったり、ギャグに走るのは性分でないためです。

ここのGitHubからダイレクトにPDFをダウンロードすることもできますが、もし私にお茶でも奢っていいと言う優しい方がいらしたら、BOOTHのコンテンツを販売しているサイトから購入していただけると幸いです。

表紙などは、商用利用OKのphotostockからPowerPoint()で加工したもので、Nate SilverさんのSignal and Noiseをきっかけに業界に入ったという背景もあって、デザインを真似ています。

内容としましては、いろんな試行錯誤をしてきたScrapingのテクニックをまとめた全集としました。

また、著作権は主張したいのですが、GitHubからダウンロードしたコンテンツを教育目的や学術目的でシェアする分には構いませんので自由に使っていただけると幸いです。

GitHub版(無料)

BOOTHでの販売

私の印刷所に持ち込むPDFの作り方が全てmarkdown + OSSだけで完結したのでその方法についてもご紹介します。

メジャーなOSSだけで日光企画さんのレビューをクリアできた

最初はAdobeの製品であったり、一部の使う機会が少ないソフトを学ばなくてはいけないのか、、、と学習コストを心配していたのですが、pandocとgoogle chrome + nodejsだけでなんとか日光企画さんのレビューをクリアする程度には品質を確保できたのでその方法をご紹介します。 MacOSで作成しましたが、Linuxでもいけるはずです。

Markdownで資料をつくる

GitHubVS CodeインタラクティブMarkdownを編集できるモードで、コンテンツを作成します。MarkdownGithubのREADMEなどを作成するときに使用できるフォーマットで、プログラムを作成する機会のある人なら大体書けると思います。

PandocでMarkdownをデザイン付きHTMLにコンパイルする

Pandocを製本のユースケースでは、あまり国内で使用している人はいないようでしたが、Markdown形式をHTML形式に変換する際にたいへん便利です。また、この変換にはCSSを指定することができ、GitHubのウェブサイトのようにコードに対してシンタックスハイライトなどを行うことも可能です。

 $ pandoc \
      -s ${インプットファイル.md} \
      -f markdown \
      --metadata title="Practical Data Science & Engineering Vol.1" \
      -c ${シンタックスハイライト.css} \
      -o ${出力ファイル名.html}

このようなコマンドで簡単にMarkdownシンタックスハイライト付きのHTML化することができます。

HTMLをPDFに変換する

最もめんどくさく、かつ、辛いのがPDF化の作業なのですが、海外のサイトなどを見ているとhtmlでフォーマットされたコンテンツをPDF化するにはGoogle ChromeのPDF生成機能を用いるのがよいとされていることが多いようです。

Google Chromeデバッグモードで起動する

 $ /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --headless --remote-debugging-port=9222

実際にGUIで操作するスタイルでは無くCUIで操作するため、Google Chromeデバッグモードで起動する必要があります。

Debug Modeで起動したGoogle Chromeにhtmlをpdfに変換する命令を行う

NodeJSをインストールしたのち、NodeJSからGoogle Chromeを操作するモジュールである、 chrome-remote-interfaceをインストールします。

chrome-remote-interface経由で以下のようなスクリプトを介してPDFに変換すると、PDFの各ページの余白やヘッダーやフッターの幅を調整したり、ページ番号を入れたりすることができます。

(印刷会社の日光企画さんに持ち込んで色々ヒアリングしたところ、ページナンバーを入れるのは必須であると言うことでした。)

    #!/usr/bin/env node
    const homedir = require('os').homedir();
    const CDP = require(homedir+'/.config/yarn/global/node_modules/chrome-remote-interface/');
    const fs = require('fs');
    
    const port = process.argv[2];
    const htmlFilePath = process.argv[3];
    const pdfFilePath = process.argv[4];
    
    (async function() {
            const protocol = await CDP({port: port});
            // Extract the DevTools protocol domains we need and enable them.
            // See API docs: https://chromedevtools.github.io/devtools-protocol/
            const {Page} = protocol;
            await Page.enable();
    
            Page.loadEventFired(function () {
                    console.log("Waiting 100ms just to be sure.")
                    setTimeout(function () {
                            //https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF
                            console.log("Printing...")
                            Page.printToPDF({
                                    marginTop: 0.8,
                                    marginBottom: 0.2,
                                    displayHeaderFooter: true,
                                    headerTemplate: '<div></div>',
                                    footerTemplate: '<div class="text center"><span class="pageNumber"></span></div>',
                            }).then((base64EncodedPdf) => {
                                    fs.writeFileSync(pdfFilePath, Buffer.from(base64EncodedPdf.data, 'base64'), 'utf8');
                                    console.log("Done")
                                    protocol.close();
                            });
                    }, 100);
            });
    
            Page.navigate({url: 'file://'+htmlFilePath});
    })();

このJavaScritpをprint-via-chrome.jsとして、実行権限を付与すると、実行できるようになるので、このように引数にchromeデバッグポート、インプットのhtml、出力ファイル名を引数にして実行するとpdfを得ることができます。

 $ print-via-chrome.js 9222 ${HTML_FILE} ${PDF_FILE}

一連のコード

DocCompile.py というファイルを作成して、pandocでカバーしきれないHTML, CSSの編集と、Google Chromeに印刷させる一連のフローをPython Scriptにラップアップしました。 自分の環境に書き換えて用いていただけると幸いです。

参考

自作レコメンドで最適な読書体験をしたい

最適な読書体験をしたい

 アマゾンなどでレコメンドされる本を上から見ていても読書体験がそんなに良くありません。
 本の売り上げランキングなどは、大衆に受ける本がほとんどであり、少々独特なセンスを持つ人たちにはそんなに受けが良くないです。
 結果として現状の解決策がSNSや人づてに聞き及ぶぐらいしかないのとジャケ買いなどがせいぜいです
 どうあるべきかを考えるとき、仮に他人の本棚を知ることができれば、集合知機械学習を用いて自分に向いているだろう本をレコメンドさせることができます

会社の技術共有会の小話で話した話

Matrix Factorization

 2000年台のNetflix Prizeからある伝統的な手法で、シンプルで動作が早く、ユーザが多くアイテムの数がとても多いときに有効な手法です。

DeepLearningでも実装できるし、sklearnなどでも関数が用意されています。

コード

自分のクエリとなる特徴量

 自分のAmazon Fionaという特定のURLにアクセスると自分の今までKindleで買ってきた本がAjaxレンダリングされます。
 Ajaxにより描画されていて、かつ、とても描画が遅いので普通の方法では自動取得できなく、google-chrome-headlessブラウザ等を利用してJSを実行しながら内容を取得できるようにします。
- 購入した本の一覧が見えるページ: https://amazon.co.jp/gp/digital/fiona/manage

実行コマンド

$ cd DataCollection
$ EMAIL=*****@gmail.com PASSWORD=***** python3 A001_from_kindle.py 
$ python3 B001_scan_local_html.py
fionaのURLをアクセスするとAjaxでこのように描画される

いろいろな人達の本棚の特徴量

 レコメンドを行うには大量のデータが必要になります。
 他人の本棚が必要になりますが、https://booklog.jp/ が本棚SNSになっているのでこれを利用します。
(すいません、スクレイピングしないと学習できないので、集めます)
実行コマンド

$ cd DataCollection
$ python3 A001_scrape.py

 現在120万ユーザが登録しているらしく、全体の一割程度でいいのでユーザの本棚をサンプルして、本棚に登録されている本を1として、登録されていない本を0とすると、巨大な疎行列を作ることができます。scipyのlil_matrixという疎行列ライブラリを利用して構築すると、400Mbyte程度に収めることができます。

実行コマンド

$ cd MakeBookReadMatrix
$ python3 A001.py
$ python3 B001.py
$ python3 C001.py

学習

一応、Matrix Factorizationにも過学習という概念があるので、2%をtestとして切り出して、ホールドアウトで、レコメンドしたときのMatrixとのMean Square Errorを小さくします。
実行コマンド

$ cd MakeBookReadMatrix
$ python3 D001.py --fit
fit non-negative matrix factorization
(1757, 1133108)
test mse = 0.000107 # <- 今回のデータ・セットではこのくらい

推論

Kindle Fionaから得られた本を、1*BOOK_NUMのMatrixに変形して、学習で作ったモデルに入力すると、各アイテム毎のレコメンドを行った際のウェイトを知ることができます。

実行コマンド

$ python3 D001.py

scores_00.csv というファイルができ、その中にタイトルとウェイトが記されている.

自分の結果

過去に漫画を大量に買っていたのでおおよそ納得の結果

別の絵本が多いユーザでもやってみましたが、絵本が多く上位に出るので想定通りできていることが確認できました。

依存(Ubuntuを想定)

$ wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
$ sudo apt install ./google-chrome-stable_current_amd64.deb
$ wget https://chromedriver.storage.googleapis.com/75.0.3770.140/chromedriver_linux64.zip # google-chromeのversionに応じたものを使ってください
$ unzip chromedriver_linux64.zip
$ sudo mv chromedriver /usr/local/bin/
  • requirements.txt
$ pip install -r requirements.txt

再現できないときいは

 よく指摘されるので、Linuxの用意や再現が難しい場合は、私のデータで学習したときのデータからモデルまでの動作が確認できたときのスナップショットがあるので、参考にしてみてください。

まとめ

 自分の知識や体験の幅を広げるには、レコメンドでウェイトが付いているが、リコールを高めに見たときに低いウェイトの方に来ている本を読むと世界や価値観の広がりを高めることができているように思います。
商業的にはおそらく高いウェイトの作品をレコメンドするとよいのでしょうが、自分に近すぎるコンテンツということもあり食傷気味であり、Amazonででる本などは興味を惹かれなかったのですが、自分でこのレコメンドエンジンを使う分にはこの制約がなくて良さそうです。