執筆プロセスを読む:自由記述アノテーションのためのキーストロークロギング
Potatoは、アノテーターが何を入力したかを記録することなく、自由記述の回答がどのように作られたかを記録できるようになりました。ポーズ、修正、貼り付けから、書かれたのではなく貼り付けられた回答を検証可能な形でフラグ付けします。
あるクラウドワーカーがあなたのアノテーションタスクを開き、文章を読み、別のタブに切り替え、20秒後に戻ってきます。そして280文字の根拠説明が、一息にボックスの中に現れます。
その根拠説明自体に問題はありません。話題に沿っていて、文法も正しく、対象の文章にも言及しています。ほかの40件と並べて読んでも、目立つところはありません。どの分類器を当てても、返ってくるのは「おそらく人間」から「判断できない」の間のどこかで、それはほとんどのテキストに対して分類器が言うことでもあります。
出来上がった回答は、それがどこから来たのかを教えてくれません。20秒と、一息の動きが教えてくれます。
Potatoはそれを記録できるようになりました。自由記述フィールドは、回答がどのように作られたかを内容に触れない形で取得し、約40個の特徴量にまとめ、その結果に少数の名前付きルールを適用できます。デフォルトはオフで、1行で有効になります。
keystroke_logging:
enabled: trueキーストロークロギング
作成、書き写し、貼り付け
これが依拠している研究は、かなり固まっています。Crossleyらは500本の議論文エッセイを集め、別のワーカーにそれを書き写させた上で、ランダムフォレストによって本人の執筆と書き写しを99%の精度で分離しました。Deaneら、Zhangらも、それぞれ独立に同じ分離を確認しています。Asherらはこれをクラウドソーシング向けに作り直し、Prolific上で、提出した文章の長さに対してキーストローク数が少なすぎる参加者にフラグを立てました。
その特徴はどの研究でも一貫しています。実際に文章を作成する場合は、文や単語の前のポーズが長く、挿入と削除が多く、キーとキーの間隔のばらつきが大きくなります。書き写しは直線的でバースト中心、ばらつきが小さくなります。貼り付けにはそもそも間隔がほとんどありません。
そのどれも、テキストからは見えません。そのすべてが、ログからは見えます。
なぜkeydownではなくbeforeinputなのか
この機能全体が成立するかどうかを決めているのが、この1つの技術的判断です。
キーストロークロガーを書くときに最初に思いつくのは、keydownを監視する方法です。そしてそれは、うまくいかない方法でもあります。貼り付け、ドラッグ&ドロップ、IMEでの変換、音声入力、オートフィル、取り消しは、いずれもkeydownを一度も発火させずにフィールドの内容を変えてしまうからです。keydownだけを見るロガーは、この機能が捉えようとしているケースをちょうど見落とします。
Potatoが主に使う信号は、beforeinputにおけるInputEvent.inputTypeです。これは上に挙げたすべてで発火し、どれが起きたのかも教えてくれます。keydownとkeyupも引き続き監視していますが、目的は別で、人が物理的に押したキーの数を数えるためです。
この2つの数値の差が、収集されるものの中で最も有用です。対応するキーストロークなしにフィールドに現れた文字はsilent_insert_charsとして、回答全体に占める割合はsilent_insert_ratioとして記録されます。ページ側で抑制された貼り付け、テキストを注入した拡張機能、音声入力のストリーム、ボックスを埋めるスクリプト。どれもキーストロークを生みませんが、どれも文字を生みます。
何が記録され、何が記録されないか
各イベントが持つのは、タイムスタンプ、入力タイプ、キーのクラス、キャレット位置、長さの変化量です。
{t_ms: 1240, input_type: "insertText", key_class: "letter", pos: 41, delta: +1}
{t_ms: 3980, input_type: "deleteContentBackward", key_class: "bksp", pos: 42, delta: -1}
{t_ms: 9120, input_type: "insertFromPaste", key_class: "unknown",pos: 43, delta: +287,
meta: {paste_source: "external", paste_hash: "sekqf3"}}
キーそのものは保存されず、どの系統に属するかだけが残ります:letter、digit、punct、space、enter、bksp、del、nav、mod、func、unknown。貼り付けられたテキストは、長さ、ソースのラベル、セッションごとにソルトを加えたハッシュに縮約されます。パスワードフィールドは端から拒否されます。ストリームから回答を復元することはできません。それがこの設計の要点で、ストリームはプロセスを記述し、内容については何も語りません。
通常の振る舞いをフラグから外しているのが、貼り付け元の分類です。貼り付けが発生すると、Potatoはそれをアノテーション対象の文章、ページに表示されているAI提案、そのフィールドにすでに入っていた内容と照合し、ラベルだけを残して照合結果は捨てます。文章の引用はinstance_textとして読まれます。自分の下書きを動かしただけならselfです。どちらも外部からの挿入には数えません。
6つのルール、モデルのふりをするものはなし
検出はサーバー側で3つの層に分かれて動きます。デフォルトでオンなのは最初の層だけです。
第1層は6つの名前付きフラグで、それぞれに明示的な閾値があり、発火の根拠となった特徴量の値を返します。
| フラグ | 発火条件 | 深刻度 |
|---|---|---|
paste_dominant | 最終テキストの半分以上が貼り付けで入った | suspect |
silent_insertion | 挿入された文字の30%以上にキーストロークの裏付けがない | suspect |
transcription_rhythm | 一定のリズムで、かつ修正がなく、かつポーズがほとんどない | review |
offscreen_composition | ページから10秒以上離れた直後に大きな外部挿入があった | suspect |
implausible_speed | 回答全体を通じて約180 wpmを超え続けている | review |
synthetic_input | ブラウザがisTrusted === falseを報告した | suspect |
transcription_rhythmが連言になっているのは意図的です。一定のリズムで打つだけなら、単に速いタイピストです。何も削除しないなら、慎重な人です。ほとんどポーズがないなら、回答が短いだけです。3つが同時に成り立ったときにだけ、書き写しの特徴が現れます。またこのルールは、読み取れるリズムがない80文字未満の回答は最初から対象外にします。
第2層はキャリブレーションです。キーストロークの特徴量は執筆課題に大きく依存するため、1文の根拠説明に合わせた閾値は、5段落の文章には合いません。python -m potato.typing_detect calibrate config.yamlは、各カットオフを自分のプロジェクトのセッションの裾のパーセンタイルに合わせて再フィットします。使用可能なセッションが最低30件必要で、キャリブレーション後の値は組み込みデフォルトの3倍以内に収められます。均質な集団が閾値を自分たちの中央値まで引き寄せてしまわないようにするためです。
第3層は教師ありです。ラベルがあれば、fit_supervised()が特徴量行列の上で実際の分類器を学習します。scikit-learnは遅延インポートされ、Potatoの依存関係には入っていません。
学習済みモデルは同梱していません。リポジトリにラベル付きコーパスはなく、何の裏付けもない係数を配布するのは、検証値をでっち上げることになります。代わりに同梱しているのは、読んで、異議を唱えて、上書きできる6つのルールです。
ラベルが欲しい場合は、トレーニングフェーズを使って自分の研究の中で作れます。アノテーターにウォームアップとして文章の書き写しをしてもらってください。そのセッションは、あなたのアノテーターがあなたのタスクで行った本物の書き写しの実例であり、通常の回答が作成クラスになります。Crossleyのコーパスもそうやって作られました。キャリブレーションの例に、この手順が一通り用意されています。
閾値はサーバー側で評価され、ブラウザには一切送られません。公開すれば、どのくらいゆっくり貼り付ければよいかをアノテーターに正確に教えることになります。
データの行き先
生のストリームはSQLiteに入ります。場所は<task_dir>/project.sqliteで、1セッション1行、メモやコードブックと同じ永続化層を通ります。イベントは差分エンコードしてzlibで圧縮され、実測で1イベントあたり1.7バイトなので、500語の回答でおよそ5 KBです。
user_state.jsonには意図的に入れていません。このファイルはアノテーションを保存するたびに全体が書き直され、長い回答は約3,000イベントになるためです。行動データにミラーされるのはコンパクトな要約だけで、キーは"{schema}:::{label}"です。これによってアノテーションと一緒にダッシュボードやエクスポートまで運ばれます。
トレーニングフェーズや事前・事後アンケートの自由記述回答も取得されます。これらのページにはインスタンスIDがないため、セッションは既存の__phase_page__センチネルの下にまとめられ、代わりにphaseとpageで識別されます。文章の書き写しという手が使えるのはこのおかげで、書き写しの実例は通常の回答とフェーズだけで区別できます。
エクスポートはどちらもオプトインです。export_include_typing_dynamics: trueはアノテーションの隣にtyping_dynamics.csvをサイドカーとして書き出し、python -m potato.export.cli <config.yaml> --format keystrokesは生のストリームをParquetに書き出します(pyarrowがなければJSONLにフォールバックします)。行動データが手違いでデータセットの公開物に含まれることはありません。
人に向ける前に
フラグは人がレビューするための材料です。証明ではありませんし、自動的な却下、報酬の差し止め、アカウント停止に直結させてはいけません。
ここでの失敗の形は、正直なアノテーターを咎めてしまうことです。そしてルールに引っかかるケースは、特殊なものではありません。対処済みのものもあります。文章を引用した場合と自分の下書きを並べ替えた場合は、ソース分類によって抑制されます。ソフトキーボードとIMEでの変換はsilent_insertionを抑制します。どちらもkeydownを確実には出さず、そうしなければすべての挿入が「無音」に見えてしまうからです。対処できていないものもあります。音声入力はフラグが立ちます。文法チェックの拡張機能もフラグが立ちます。一部の支援技術は信頼されないイベントを生成し、synthetic_inputに触れます。ドキュメントで、支援技術のユーザーに開かれた研究であればこのルールはオフにすべきだとはっきり書いているのはそのためです。仕事に必要な道具を使っていただけの人にフラグの説明をするよりは、そのほうがましです。
そして算数の問題があります。回答の5%が貼り付けで、ルールがセッションの5%にフラグを立てるなら、フラグが付いたものの大半が正直な仕事であることは十分にありえます。不正が実際にはまれなプラットフォームでは、偽陽性率がそれなりに低いルールでも、正しく捉えた件数より誤った告発のほうが多くなります。キャリブレーション済みの閾値はこれを改善するのではなく、明示するだけです。裾のパーセンタイルは、誰も不正をしていない集団を含めて、どんな集団でもその裾の割合にフラグを立てます。
開示はデフォルトでオンで、オフにすると起動時に警告がログに出ます。このデフォルトがあるのは、タイミングのパターンが行動的なバイオメトリクスだからです。個人を特定でき、文脈をまたいでアカウントを結び付けることもでき、研究文献ではタイピング技能、第二言語話者かどうか、認知的負荷の推定に使われてきました。文にラベルを付けるつもりで参加した人が、そこまで想定していたはずはありません。Potatoはそうした推定を一切計算せず、そのためのツールも提供しませんが、保持したデータはその分析を可能にします。それはツールではなくあなたが管理すべき問題です。typing_store.delete_for_user()は特定の参加者のストリームを削除し、fidelity: summaryは特徴量を残したままバイオメトリクスの詳細を落とします。倫理のページに、同意文の例、保持に関する指針、GDPR第22条、IRB審査、プラットフォームの却下ポリシーについての注記があります。
writing_process_riskは、管理者ダッシュボードの執筆プロセスパネルに、既存のsuspicion_scoreとは別の並べ替え用の指標として表示されます。どちらの数値ももう一方に混ざらないので、片方が他方の意味をひそかに変えてしまうことはありません。
有効にする
この機能はPotato 2.7.2に入っています。冒頭の1行より詳しい設定は次のとおりです。
keystroke_logging:
enabled: true
fidelity: events # off | summary | events
include_schemas: [rationale] # empty means every free-text field
disclose_to_annotators: true
detection:
enabled: true
on_external_insert: flag # allow | warn | block | flagon_external_insert: blockは、計測対象のフィールドへの貼り付けを禁止します。ただし正当な引用も同時に止めますし、その気になれば手で打ち直せるので、多くの場合はflagにしてレビューするほうが割に合います。
実行できるプロジェクトがexamples/advanced/keystroke-logging/に、キャリブレーションの手順がexamples/advanced/keystroke-calibration/にあります。
ドキュメント
- キーストロークロギング — 取得されるすべてのフィールド、要約特徴量、保存先、トラブルシューティング
- 執筆プロセス検出 — 6つのルール、3つの層、偽陽性の一覧、引用文献
- キーストロークロギングの倫理 — 同意、IRB、データ保持、参加者の権利
- 行動トラッキング — この機能が組み込まれている、より広いインタラクション追跡の仕組み
- 品質管理 — 注意チェックとゴールドスタンダード
- 管理者ダッシュボード — 執筆プロセスパネルの場所
- ProlificとMTurkでのクラウドソーシング — モニタリングと却下に関するプラットフォームの規定
アップグレード
pip install --upgrade potato-annotation==2.7.2既存のプロジェクトは、あなたが指定するまで何も変わりません。keystroke_logging.enabledのデフォルトはfalseなので、アップグレードによって誰かの記録が始まることはありません。