2021年6月24日木曜日

形態素解析と「すもももももももものうち」

「きしゃのきしゃがきしゃできしゃした」は知らなかったので、自分用メモです。 私はこの手の話については「すもももももももものうち」自体がヒューリティクスという認識でいます。

そもそも、すももは「桃のうち」ではなく、実は桃とは異なる、バラ科の別の植物です。 つまり誤文で、私はこれを知った時にがっくり来ました。 ではなぜ「すもも」になるのかと思ったことがあります (キレ気味)。 形態素解析は統計順で行うから「すもも」が正しいのですと言う主張も、納得感がありません。 そもそも間違ってるし、別に統計順を知りたい訳でもない。 どちらかと言えば「酢も藻も桃も股の内」のほうが間違いがない。 まあこれも「も」が多すぎるので lint 的にはあまりよろしくないのですが。

このような問題を考えていくと、完全に意味論の話になってて、形態素解析という枠組みではもう解けない問題です。 よって形態素解析器の平仮名問題は考える必要がなく、日本語の lint で検出すべき問題と思います。 ひらがなの連続回数やひらがなの文字列パターンで、読みやすさが決まることは明白なので、警告を出せば良い。 形態素解析器の精度の議論にも含めてはいけないと思う。 さらに言えばそのへんも考慮して形態素解析すべきなのだと思います。

この話って、たぶん話し言葉の解析に通じます。 話し言葉は Tiny Segmenter で 95% の精度が出て、Mecab も同じくらいの精度です。 そして数GB の rank 情報を無理やり入れても 98% なので、そもそも形態素解析器とは…うごごご…と思ったりします。 平仮名をしっかりすることのほうがよほど重要ではないのかと。 これはずっと思ってることなんですが、Tiny Segmenter 周りってもっと深堀りしたほうが良いんじゃないのかなあ。 3-hop で 95% が出るので詳細なラティスはいらないと思います。 個人的には品詞もうーんと思ったりするのですが、そこはさらに意見が分かれそうかなあ。

いろいろと良い手法が浮かびはするのですが、巷の検証用コーパス自体が有料なので、やる気がしないんですよね…。 誰か Deep でポンしてくれないかなのところがある。

2021年6月20日日曜日

フリゲ紹介: SOLDIERS -DesireWing-(Trial ver)

SOLDIERS -DesireWing-(Trial ver) はまだ体験版ながら、以前から興味のあった長編 (予定?) RPG です。 現在は7章まで3時間くらいの作品ですが、今後に大期待です。



神に機械が禁じられた世界で、機械を作りつづける狂人集団『レア教団』。 それを撃滅するは、最も女を捨てた女…神の使者『ラクトザルド』。

これは古き良き時代の厨二成分が凝縮された芸術作品ですね…。 唐突に次元切断して物語が始まったり (左)、ボスが一瞬で首芸術になったり (右)、 重要人物っぽい人が数分後にコロッとやられてたり、 超絶に濃ゆい文章とストーリーで殴ってくるタイプの RPG です。 かつてこれほどまでに疾走感があるストーリーの RPG はあっただろうか。たぶんない。



なんとなく北斗の拳や忍殺の成分が強いような気がします (下)。 フリゲをやってる人だとメイジの転生録にテイストが近いかも知れませんね。 ストーリー重視の RPG は、やっぱこういう才能爆発系のほうが好みです。



ゲームバランスのほうは唐突に恐竜に襲われて全滅したり、 いきなり壊れ性能っぽい武器防具が手に入ったり、なかなか尖ってます。 しかしサイドビュー+ロマサガっぽい成長システムが実装されてて実はすごい (左)。 バランスも意外と良くて面白かったです。

戦闘面以外でも何気に細かいところに凝ってて、強敵の特徴を死体から観察して対策を取ることができます (右)。 これは今まで見たことなかったのですが、死と隣り合わせの世界観の中ではまさに重要。 すごく良い発想だなと思いました。



だいぶ前のだけど作者様の紹介動画がこちら。この PV 最高です。



完成版が楽しみです。

フリゲ紹介: ジャンヌアクション

ジャンヌアクション は RPGツクール2000 のジャンヌをあやつり、敵をひたすら斬っていく爽快感のあるアクションゲームです。 最近、RPGアツマールでもプレイできるようになったようで存在を知り、プレイしてみました。



最初はなんとなく強そうなドラゴンソードを求めて冒険に出たジャンヌでしたが、 気付いたらドラゴンソードの争奪戦に巻き込まれることに。 RPGツクール2000 でもこんなにしっかりしたアクションゲームができるんだなあ…と思わされた作品です。 作者様は他にもテイルズっぽいゲームや、スマブラっぽいゲームも作ったりしてます。すごい。

難易度は難しすぎず簡単すぎず、ちょうどいいくらい。 アイスソードやフレイムソードなどいくつかの属性の剣を操りながら、攻略していきます。 すこし変わっているのが、ソードで攻撃すると滞空時間やジャンプ力が伸びるところ。 これを利用して、山登りしていくようなステージもあります。



アクション面では唯一どくどくタワーっぽいステージが割とシビアで、そこで 50回くらいやられました。 ジャンプを細かく制御するのは結構難しかった。 あとボス戦もすこし難しめですが、何度もトライするとジャンヌのレベルが上がっていくので、 こちらは何度もトライすればクリアできるようになってきます。 プレイヤーの腕前に合わせてじょじょに難易度が下がる仕組みっていいですね。



RPG アツマールのほうは少し難易度が下がっているらしいので、そちらでプレイしてみても良いかも知れません。

2021年6月12日土曜日

JavaScript で音声再生まとめ

個人的に毎回ハマっている JavaScript の音声再生と HTML Audio / Web Audio API / AudioContext / unlockAudio についてまとめておきます。 重点的に取り上げるのは同一音声を連続再生する時のテクニックです。 どこで使えるかというと、ゲームを作る時の効果音などです。

同一の音声を同時に鳴らす様々な方法

(1) 何も考えないと以下が浮かぶと思います。BGM として流しっぱなしにするならこれで良いんですがね。 毎回オブジェクトを生成するので、iOS が激重とわかりました。
button.onclick = () => {
  new Audio('test').play();  
}
(2) といって以下はボタンを連打した時に音が一つしか鳴らないので駄目です。
const audio = new Audio('test.mp3');
button.onclick = () => {
  audio.play();  
}
(3) 1 よりずっと軽いですが、まだ重いです。
const audio = new Audio('test.mp3');
button.onclick = () => {
  audio.cloneNode().play();  
}
(4) 高頻度で呼び出されることがわかっている場合は、事前に cloneNode() しておくと良いかも知れません。 これは早いけどメモリをそれなりに食う。
const audio = new Audio('test.mp3');
const audios = [...new Array(10)].map(() => audio.cloneNode());
button.onclick = () => {
  const a = audios.find(a => a.paused);
  if (a) { a.play(); }
}
(5) AudioContext を使う方法がおそらく一番良い実装です。 体感的に 1-4 のどれよりも高速に動作します。ただし実装が面倒くさい。
const AudioContext = window.AudioContext || window.webkitAudioContext;
const audioContext = new AudioContext();

function playAudio(audioBuffer, volume) {
  const audioSource = audioContext.createBufferSource();
  audioSource.buffer = audioBuffer;
  if (volume) {
    const gainNode = audioContext.createGain();
    gainNode.gain.value = volume;
    gainNode.connect(audioContext.destination);
    audioSource.connect(gainNode);
    audioSource.start();
  } else {
    audioSource.connect(audioContext.destination);
    audioSource.start();
  }
}

function unlockAudio() {
  audioContext.resume();
}

function loadAudio(url) {
  return fetch(url)
    .then(response => response.arrayBuffer())
    .then(arrayBuffer => {
      return new Promise((resolve, reject) => {
        audioContext.decodeAudioData(arrayBuffer, (audioBuffer) => {
          resolve(audioBuffer);
        }, (err) => {
          reject(err);
        });
      });
    });
}

function loadAudios() {
  promises = [
    loadAudio('test1.mp3'),
    loadAudio('test2.mp3'),
    loadAudio('test3.mp3'),
  ];
  Promise.all(promises).then(audioBuffers => {
    audio1 = audioBuffers[0];
    audio2 = audioBuffers[1];
    audio3 = audioBuffers[2];
  });
}

let audio1, audio2, audio3;
loadAudios();
document.addEventListener("click", unlockAudios, { once:true, evnetCapture:true });
button.onclick = () => {
  playAudio(audio1);
}
コードにしてしまうと簡単ですが、色々と注意点はあります。 (1) AudioContext は PC/スマホ共に、クリックやタップなどのユーザ操作後にしか使えません。 そこで音声データを fetch した後、unlockAudio を仕込んで音声を再生できるようにする必要があります。 (2) iOS では Promise ベースの decodeAudioData() は使えません。 というより後述する HTML Audio ではユーザ操作の間に非同期処理を挟むと再生されなくなるので、たぶん使ってはいけません。 (3) unlockAudio を仕込む場所にも注意が必要です。 iOS の後方互換性を考慮する必要があります。 非常に闇が深いので、以下も参考にすることをおすすめします。

unlockAudio について

HTML Audio は iOS ではクリックやタップといった操作の後、同期的にしか Audio 再生ができません。 事前に音声をロードさせておかないと、Audio を再生できずに困ることがよくあります。 そのため、タップイベントに音声の初期化処理を仕込んで音声をロードする、unlockAudio の手法がよく使われます。 HTML Audio の場合、私がよく使っているのはこちら。 HTML Audio の unlock 問題が起きるのはスマホやタブレットだけなので、touchstart に仕込むのが良いかと思います。 最近は IE を無視できるようになってきたので、{ once:true } が使えるようになりました。
function unlockAudio(audio) {
  audio.volume = 0;
  audio.play();
  audio.pause();
  audio.currentTime = 0;
  audio.volume = 1;
}

function unlockAudios() {
  unlockAudio(audio1);
  unlockAudio(audio2);
  unlockAudio(audio3);
}

const audio1 = new Audio('test1.mp3');
const audio2 = new Audio('test2.mp3');
const audio3 = new Audio('test3.mp3');
document.addEventListener("touchstart", unlockAudio, { once:true });

タップイベントの 300ms delay について

ここまでの実装例でもわかるように、click や touchstart などのイベントに unlockAudio を仕込む必要があります。 なるべく早く読み込みたいのですが、スマホやタブレットのタップイベントには 300ms の delay が含まれることが知られています。 タップイベントの delay は 以下の方法でなくすことができるので、読込が早くなります。
<meta name="viewport" content="width=device-width">
といっても上記のタグはほとんどの人が書くようになったと思うのであまり深く考える必要はありません。 念のため以下のようにタップイベントを限定しても良いと思います。
#button { touch-action:manipulation; }
他にもいくつか delay を消す方法がありますが、サポート状況なども含めて詳しくは上記のページを参考にすると良いでしょう。

2021年6月5日土曜日

Sentency: 英文並び替えアプリを作った

英文並び替えアプリ Sentency を作りました。 よくある並び替え問題ですが、音声読み上げが付いてて、多少のゲーム性があります。



先に 英文タイピング を作りましたが、タイピングだと細かな入力ブレの調整が大変だったので、もう少し簡単なのがあると良いと思いました。 普通の並び替え問題ならスマホでもできます。 完成したもので遊んでみると、これはこれで良い感じ。 タイピングの EASY モードの次はこちらのほうが良いし、小学生なら絶対にこちらが良い。 きちんと理解していれば思考をほとんど邪魔されず、サクサクと解けます。

受験を意識した学習より良いかも

小さな子でも遊べるように難易度調整してみると、小中学校の高校受験を意識した学習より良いかもと思いました。 高校受験はテンプレ文法がたくさんありますが、それを習っただけだと海外の幼児用図書を読むのも大変です。 それなりにレベルの高い高校英語を学んで、その後やっとフリースタイルの文法を習って、高校の中間でようやく幼児書が読めるようになる。 しかし今後の大学受験の英文は口語に近づいてきている現状もあり、 今の高校受験のテンプレ文法だと色々無理なことが増えると思っているんですが、 小中学校ではそのへん意識されてない気がするんですよね。 それだったらフリースタイルに近い文法から習い、英語を早く変換する方法を学んだほうがいいのではないか。

ただそれは理想論で、色々なデータやコーパスを見てるとそう簡単にできないこともわかってます。 海外の幼児書より簡単な本とかほとんどないですからねえ。そのへんが日本の英語学習の本質的問題とも言えます。 前々からもっと良い学び方はないかと思っていたので、作ってみたのが Sentency です。 Sentency は学校のように文法を少しずつ学ぶのではなく、高速に例文を叩き込みます。 例文をレベル別に山ほど見ていれば文法は勝手に覚えるだろうというスタイルです。 これは適当という訳でもなくて、第二言語学習に効果的と言われるセンテンス・リピーティングを意識した学習法です。 日本語も文法を習って覚える訳じゃないので、学校の覚え方が正しい訳でもない。

センテンス・リピーティングの良さ

センテンス・リピーティングは最初にかなりの情報量を叩き込まれるので初学者は少し大変ですが、私は 2020年の英語教育改革でだいぶやりやすくなったと思っています。 小学生はまず最初にふわっとした英文を小4から学ぶので、あとはそれをどこかの段階で体得するまでやれば良い訳です。 ちなみに英語学習の早期化の問題は、よほどうまくやらないと何となく聞いているだけ、やってるだけになる子が増えるというところだと思っています。 それだったらすべての時間を作業にしたほうが効率的だし、その問題に対する解答の一つがセンテンス・リピーティングだと私は思っています。 幼児期はドリル演習って凄い効果的ですが、その本質は聞いてるだけではなく作業することにあるのではないでしょうか。 センテンス・リピーティングはドリルとして非常に効率的です。 レベル分けも綺麗にできるし、学習が早いし、実用性も高いし、正確だと思うんだよなあ。

最初にふわっとしたものを何となく学ぶ今の学校教育にもセンテンス・リピーティングはフィットすると思ってます。 必須語英単語 を 50個きちんと覚えるという事前準備だけ必要ですが、それ以外はふわっと例文を覚えるのと本質的に変わらないからです。 英単語は最初の 100語覚えただけで、文法的にはほとんどの英文を読めるようになります。 なので最初の 100語だけを小5~小6 までにマスターし、あとはふわっと例文として覚えられるようにしておけば、 実は中学生以降の勉強は単語レベルを徐々にあげていくだけで済みます。 中学生で学ぶ文法の話はほぼすべてを飛ばして考えることができるんですよねえ。 これはとても効率的と思いませんか?

まとめ

以上、面白そうだったので Sentency を作りました。 英単語アプリの Vocabee と組み合わせて学習すれば、非常に効果的な勉強ができると思います。

ちなみに例文はいくつかの対訳コーパスから明らかに間違っている翻訳を削除し、 性的表現や冒涜語などの禁止用語を削除し、レベルごとに整頓して作っています (下)。 量が量なので完璧を求められても無理ですが、報告頂ければ直します。

英文タイピングを作った

英文タイピング を作りました。 英文を入力するだけではつまらないので、勉強とセットのゲームです。



英熟語なる概念は存在しない?

最初は英文より英熟語のタイピングを先に作ろうと思ったのですが、色々考えてみるとあまり良くない。 英熟語のリストを作っている途中で、英熟語ってほとんど意味ないなと感じました。 中学生の頃はそういうのも真面目に覚えていたものですが、そもそも英語と日本語では文法や熟語の概念自体が違います。 syntax/idiom も日本の概念とは全然異なるので、日本で言うところの英熟語に当たるものは見当たりません。 of course や look after みたいなのは、海外では英熟語とは言わないです。 日本で言う英熟語に一番近いのは collocation とは思うのですが、ぴったりの概念は存在しないような気がします。

英熟語という概念そのものが存在するのか検索してみると、Wikipedia に良いまとめがありました。 このページには割と本質なことが書いてあって、とても参考になりました。 やはり日本と英語の文法の考え方はまったく違う。 私は日本の英熟語の勉強って単語の理解不足で起きる無駄な作業と思ってるんですが、 Wikipedia にも同じようなことが書いてありました。 そうそう「look for = 探す」で良いのは高校受験までで、大学受験では間違いにされるんですよね。 やはりやってはいけない覚え方だよなあ、と思ったりします。

英文の高速練習の重要性

ではどうやって英語の文法的なものを覚えるのが良いでしょうか。 色々考えてみたのですが、文章を高速に物量で叩き込み、イメージで理解するのが良いと思いました。 英語学習ではよく負荷が大事と言いますが、まさにそれで、短時間に一気に負荷を掛けて後から吸収することが必要と思ってます。 なぜなら英語をきちんと理解するためには、英単語列を見てどれだけスムーズに理解できるかや、 可能ならば日本語への変換もしたくないという話もあるからです。 タイピングで短文を叩き込むのは、それなりに良さそうです。

タイピング+文法学習には一工夫必要あり

しかし実際やってみると結構コツがいることがわかりました。 英文をループで流しっぱなしにしながらタイピングすると、頭に入ってきません。 英単語と異なり、英文は入力に時間が掛かるため、集中力を奪われてしまうようです。 あと英文の読み上げが早いので、そちらに集中力を取られて、タイピングの思考速度と合わない問題があります。 速度を落としたり可変にすることも考えましたが、うーんな感じ。 英文を聞いた後にそれを予測して打つならアリですが、聞きながら打つのはやはり合わない。

ということで最初に2回だけ読み上げ、その後に頭の中で整理しながらやると、まあまあ良い感じ。 まず最初に英文を全部頭に入れてからタイプするほうが、ミスも少ないし圧倒的に早く打てます。 実のところ日本語のタイピングでも同じことが重要なので、言語を理解するのってそういうことなんだなと、やってみて感じます。 英文のタイピングになると大きく速度が落ちる人は多いですが、それは言語理解速度の遅さに起因していることがよくわかります。 英文を高速に頭に入れることを鍛えるアプリと思ってやると、活用しやすいアプリになってるかも知れません。

HARD モードはめちゃくちゃ難しくて、よほど英語が得意でないとクリアできないと思います。 小学生の範囲でも細かな聞き取りは難しいので、かなり苦労します。 さすがにクリアできる人が少な過ぎる気がするので、NORMAL モードも作っておきました…。 しかし NORMAL モードでもタイピングが追いつかなくて結構きつい。 過去形とか複数形とか冠詞とか、短縮形で発音しているかなどを高速に判断して入力するのって思ってるより難しいんだなと。 勉強としては効果が高いかも知れないけど、タイピング速度は壊滅的になりそう。 ほとんどの人は EASY モードより上に行くのは難しいんじゃないかなあ。 普通のタイピングゲームでタイプ速度が 4key/sec 以下だと、NORMAL 以上は少し大変かも。

2021年6月1日火曜日

2021年は IndexedDB 元年

Vocabee を作るに当たって初めて IndexedDB を触りました。 よくできてる。

IndexedDB でできるようになったこと

メモリ上の操作と比較するとさすがにデータ保存に若干のラグがあるので、 UIに影響のないように非同期で投げておき、コールドスリープ的な利用をする API になるかと思います。 IndexedDB ができたおかげで、ブラウザ上にいくらでも、気楽にデータを永続化できるようになりました。 ユーザ側でデータを溜めておいて、必要な時だけサーバにpushすればいい。 このとき小規模データならpushするサーバは自分で用意する必要がなく、 オンラインストレージなど色々な選択肢がある。 代替サービスがたくさんあるのでサービス停止リスクも今ならない。 仮にサーバを持つとしてもpushの頻度をいくらでも制御できる。 ようするに運用コストを思ったようにコントロールできるようになってきました。

2017年〜2019年 にオンラインストレージ API が揃った

私は 2020年に Photo Scanner というアプリを Dropbox 依存で作った時、オンラインストレージ依存は結構良いなあと思いました。 1年経って今度は Google Spreadsheet を使って Vocabee を作ってみて、やはり良いなあと感じました。 感触が良いのでこの実装方式っていつからできるようになったんだろうと思って調べてみると、案外最近です。

まず OAuth 2.0 の RFC は 2012年、Google OAuth 2.0 は 2016年 でした。 オンラインストレージサービスの対応は早く JavaScript SDK ができたのは、BOX, pCloud が 2016年、OneDrive や GoogleDrive が 2017年、Dropbox が 2019年でした。 データをオンラインストレージ任せにする発想自体は 2017年には現実的にはなっていたものの、有名企業が勢揃いしたのは 2019年。 他にも色々オンラインストレージサービスはありますが、いまのところ API で使えるのはこのへんかな。 ほどほどに新しい実装方法になるみたいです。

IndexedDB のスマホ対応は 2021 年

IndexedDB の仕様が固まったのは 2015年、デスクトップでサポートが安定したのは 2020年、 スマホでサポートが安定したのは 2021年と、こちらは予想以上に最近です。 このへんの事情は Can I use... の date relative のところを見るとよくわかります。 つまりレベルの高いデータプーリングができるようになったのは本当につい最近の話。 2021 年は IndexedDB 元年とも言える状態になっています。

スマホで高度なデータプーリングがしやすくなると何が起きるでしょうか。 私が真っ先に思い付いたのは先に語った通りで、フロントエンドでプーリングすることで負荷を下げやすくなる効果です。 OAuth でオフライン作業できなかったところを保持しておいて、後で投げるみたいな。 あとは私が作ったみたいに個人情報をすべて各自が管理できるところようになることだと思うんだよなあ。 個人開発だと個人情報なんか欲しくもないし集めたくもないのにサーバに保存しなきゃいけない事が多く、いつか爆発するかも知れないただの地雷、負債であることが多いです。 しかしOAuth 経由でユーザ自身に個人情報を保存させれば、そんなことに悩む必要がなくなります。 ほかにはゲームのセーブデータ管理かな。これもわざわざサーバに保存する必要性がなくなる。

何気に大きな変化になるかも

静的サービスは既に無料が基本になりつつありますが、小規模な write が発生する動的サービスも無料でいけるとわかりました。 これまでは小規模な write でも VPS やサーバーレスの DB が必要でした。 しかしオンラインストレージや Google Spreadsheet を利用すれば、今後は自前でサーバを持つ必要も、課金リスクのあるサーバーレスサービスも必要ありません。 サービス側もバックエンドで下手な負債を抱えずゼロリスクで構築でき、さらに無料放置できるため、より楽で現実的な実装と言えます。 個人レベルから中小企業程度のサービスなら運用コストをいくらでも減らせる、とても良い時代になりました。 逆に言えばバックエンド戦略はフロントエンドありきの時代とも言えますね。

使い勝手の所感

Vocabee を作って遊んでみた結論として、Google Spreadsheet は範囲を固定化したシーケンシャルな読み出しに強いですね。 OAuth で複雑なことをやるのはレスポンスに問題が出てきそうなので、アクセス回数は1-2回にしたい。 RDB のような最適化はせず、なるべく1回のアクセスでドカッと読み出して、後は計算するほうが処理も速度も早い。 シーケンシャルな処理で速度を保てるのは一般に〜数千件なので、そのへんを意識すると良さそう。 オンラインストレージも同じことが言えると思います。

OAuth は最初の判定に 1秒くらい掛かるので、データの読込はどうしても遅くなります。 一方、IndexedDB は openDB に 50〜100ms くらいしか掛かりません。 厳密なトランザクションを維持すると大変なことになるので、私は適当に非同期で書き込みし、同様に一定レベルだけ保って読み込みしてます。 読み込みする時は、ひとまず IndexedDB で読み込んで、非同期で後から内容を上書きするような利用が良いと思います。 ここでも厳密にやり過ぎると大変なので、多少の不整合は覚悟しながら、影響範囲を少なくする。 そして色々なタイミングで上書きしやすく、また不整合が起きにくいような実装にすると良さそう。 Google Spreadsheet はデータが存在しないとその列はスキップされる特性があるので、 だいたいのケースでは、オンラインストレージ側と IndexedDB 側の状態を管理する配列に書き込んでから DOM を更新するのが一番簡単。 DOM だけで処理しようとするとうまくいかない。