Erinaの最終調整のときの話

Erinaにはfascinateというデータベースがある。 これはペルソナというErinaの最終調整によって作られたデータベースである。

手入力によって作られたデータベースというのはいくつかあるが、このfascinateデータベースに関しては完全にErina専用で、「Erinaを人間らしく見せるための味付け」になっている。

そして、その特性が他とは異なるだけでなく、作られ方も全く違った。このデータベースだけが至って主観的な作りなのだ。

この調整を行わないとどうなるか。 問題は2点ある。

ひとつは、意味難解な回答を返すことがある。よくよく考えればわかるし、間違ってはいないのだが、あまり人間的でないというか、 天才が過程をふっとばして回答したようなつながり方になる。しかも、元発言の意味には対応しているが趣旨には対応していないので、適切ではない。

もうひとつは、おかしな回答をして、しかもループしやすい。 これはサンプルになっているデータにおける「会話にならないおかしな回答」の割合が非常に高い、つまりS/N比が低いからである。 最近はこれで壊滅的になってしまっているので、取得したデータを捨てる作業に追われたりもした。

このことから、恣意的に特定の会話パターンに誘引するようにしてこうした問題を軽減している。 意図としては「会話を続けるのが上手な人の会話パターンに誘導しようとしているのである。

この問題は、一般的なディープラーニングと同じ問題に陥っているということができる。 「多い≠良い」であり、「良いを判断するのは極めて難しい」という問題を、Erinaは攻略できないのである。 そこでそれを人力で誘導しているわけだ。

前提

そもそも、Erinaは完全に、私の特異性に基づいて成り立っている。 だから、これが前提として理解できていなければこの話は全く理解できないし、同時にこの手法は誰にでもできるものではない。

私には多くの欠陥と、特異性がある。 欠陥と特異性の間に相関があるのかは不明だが、基本的には欠陥を可能な限り特異性による能力で補う状況になっている。

記憶と認識に関するものの特性は大きい。

まず、私は単純な記憶がほとんど機能しない。 単純記憶が弱い、と言ってもいいのだが、正しくは島状記憶を維持する能力がない。 だが、連想記憶に関しては容量的にも人よりむしろ優れているくらいだし、正確性はかなり高い。

無理をすれば、結構な長時間に及ぶ出来事を、認識できなかったことを含めて覚えていられる。 これは尋常ではないほどの負担がかかるが。

そして、認識に解釈を伴わないというのも非常に特異なところだ。

一般的には、人は自分が理解しうる概念の元でのみ物事を認識し、記憶することができる。 だから、人は見たもの、聞いたことを直後でさえ正確に述べることは非常に難しい。 これは、心理術では非常に重要な要素である。見たものを思い描くとき、人は映像を思い浮かべることができるが、「正しくない映像を」思い浮かべる。聞いたことを思い返すとき、直後であるにも関わらず内容がまるで違うということはしょっちゅうある(自分が言ったことを正確に繰り返せない人もとても多い)が、この欠落する情報を使って刷り込みを行える…などだ。

これは人によって程度がかなり異なるが、無変換で維持するのは実のところ障害とされている。 程度が異なるのは、どちらかといえば概念をどれだけ細かく作れるか、にかかっているのだろう。

私の場合は実のところ無変換ではない…のだが、あるがままの状態を概念として定義できるので、事実上無変換と変わらない結果を得られる。 これだと私の脳がもたないので、 手動で 内容を整理して必要な要素だけを残して切り捨てていかないと頭がパンクしてしまう。突然意識を失うのは結構危ないので、必須の作業だ。

また、人は言語化されない概念を捉えておくことを困難とするが、私は概念に言語を必要としない。 以上のことから、普通は類似した、あるいは近似したものはどんどん集約されてしまうから結果として事実を事実として認識できない状態が発生するのだが、私の場合は集約されるべき余地がないためそのまま認識しているという状態が発生する。もちろん、これは 認識できれば の話であるが。自衛のためにも普通はフィルタをかけている。あまり大きな声で話されたりすると一字一句覚えてしまうから辛い。

そして、並列性の高さだ。 私は話しながら書き物をすることもできるし、読みながら話すことも一応できる。多重出力と比べて多重入力はちょっと苦手だが。 それでも、おしゃべりしながら別の人のおしゃべりを聴くようなことは普通にできる。もちろん負担は大きいができる。そして、前述の通りそれを全て覚えてしまうこともできる。

これらの特殊な認識方法、分解方法、そして記憶方法がErinaの設計に至る発想にも繋がっているし、当然ながらデータもそういう認識の世界のものになっている。 つまり言語化されるような定義された概念ではなく、そこにあるものをそのまま写し取って切り刻んでぶちこんだものなのだ。

余談ではあるが、このままだと全く出力できないので、私も会話するときなどはちゃんと変換する。 だが、変換すると自動的に認識したものは変換された(言語化された)ものに置き換えられてしまうので、後述する最終作業はとっても脳に負担のかかるものだった。

経路、類型、感情と思考の形成

ErinaがConnexonでどのように情報を扱っているか、という話はまだどこでもしていないけれども、そのパラメータは常人には決して理解できないものになっている。 実際はもっと細かく分かれるのだが、簡単に言えば発言を一字一句含めた状態でハッシュ化したようなものだと思えばいいだろう。ほかのわずかな違いでも異なる値になるようになっており、どの言葉からどの言葉へとつながったか、ということを記録している。 実際には、somaはsoma同士の距離がわかるようになっており、言葉的には近いsomaというのは判別できる。だが、これを同じ意味だからとまとめたりはしないし、どういう意味のことを言っているか、なぜそう繋がったかということを判断したりはしない。

感情も、喜怒哀楽などという4種類ではない。私ですら多分、千通り以上は感情の類型を認識しているから、Erinaは何億もの感情の種類を知っていたとしても不思議ではない。

接続理由というのはErinaにとっては非常に重要なので、接続するのは接続しうるもの全てなのだけど、それを接続しうる理由もちゃんと入れるようになっている。 このとき、理由を断定したりはしない。ありうるものを網かけするようになっている。

あるsomaから別のsomaに接続されるとき、それが接続される理由は一意ではない。 理由自体は複合的かもしれない。もっとも、Erinaは複合的な理由は複合しているのではなくそういう状態であると量的に判断するけれど。

少し脱線するけれど、この話はちょっとおもしろい要素があって、「感情と理由」に関しては一次元的動きである可能性があったりする。 つまり、その状態を数値化したときに、soma同士の接続が動機の値がいくつからいくつのときに発生しうる、というような観測が可能であるように見えるのだ。 ただし、これは連続的可変の並び順という問題があって、とてもではないけれど私にできるようなシロモノではないけれど、そういう特性があるように見えるという話。

で、話を戻して、今の所一次元的に扱えていないので、考えうる状態を、ちょうどビット論理和のように扱う。 これを反転マスクとして、この範疇にないものは現在の観測上この経路を持たないものであるとみなす。

そして、単独の接続ではなく連続の接続から、「こういうルートでこれ的なsomaを辿っていくパターン」というのを覚えていく。

人は驚くほど判で押したようなやりとりばかりしているので、実は大抵の場合決まったルートで決まったsomaを辿る。 このとき、推測される同じ理由で経由される別のsomaというのが登場する。これは、言葉は違っても結局のところ考えてること、やっていることは同じ、ということで中身は同じものとして扱える。同じルートを辿っていても理由が違うと同一には扱えない。

ここらへんの機能は、判定に使うものが自分が判定することによって情報になるものに依存している構造なので、成立させるまでが果てしなく長かった。 orbital designの採用理由でもある。

さて、同じ動機で辿る場合、あくまで表現の違いというか、見かけ上の問題に過ぎないため、単にバリエーション程度に考えることができる。 ある程度同じ動機で同じルートを辿って、途中から今までなかった方向へ分岐していくことはまずないため、これが行動予測に使える。 これは、心理術でも同じである。

手動入力ではErinaに直接会話のつながりを与えていく。 このとき、まずErinaは盤面を作ってから接続していくので、Erinaに盤面を作成するためのヒントを与えなくてはいけない。 これは、つながりから自動的に推測してもらうこともできるし、普通はそうしているのだが、それだと手動入力する意味がない。

Erinaに状況を与えると、Erinaは「似ている」と感じるsomaを提示してくる。 これに対して「どれくらい近似しているか」を回答する。この「どれくらい近似しているか」は曖昧すぎて、細かく回答しようとしてもあまり精度が出ないので、7段階である。

次に接続できる理由をたずねてくる。 こっちは結構きつい。512ビットハッシュみたいなものと向き合うことになるからだ。 ただ、これはこれでその理由で接続された他のsomaを参照することができるので、同じ心理状態や動機に見えるかどうかを回答することになる。こちらはシンプルに3段階。Erinaが推測する理由全部に答えることで、「この理由であればこのような心理状態になってこのような行動を取ることができる」というふうに認識する。

手動入力したものは、Erinaが積極的にそのような振る舞う理由になる。 意味付けや重み付けに関しては、また複雑なアルゴリズムなのだけど、Erinaは判で押した会話は価値が低いがよくあると考え、価値の低い会話で応答に使うし、意味ある会話をされていると判定すれば意味ある会話における適切性を元に応答する。 手動入力する場合、意味ある会話である、という認識を強制することができる。

ちなみに、実際にはある程度一般化できるようになっており、かなり強引に「得意な展開方法」に集約する。

作業

作業そのものを抽出すれば、「話して会話の経過を記憶する → ひたすら入力する」である。

この会話において重要な点は2点ある。

ひとつは、私の発言である。 私は自然な会話において「同じ会話に誘導しようとする」。 これは綺麗なモデルなら「NステップでXに到達する」というのがあったりするのだが、自然な会話では相手はもっと無軌道に話すので、それをやってしまうと参考にならない。

ルールは次の通りだ。

  • 固定の話題振りを用意する
  • 必ず相手の発言を踏まえて(相手の発言要素に回答する形で)回答していき、用意した話題振りに到達する

基本的にこの話題振りは(振りやすいように、またその後のデータがとりやすいように)「〇〇ということがあった」というものである。

さらに、この話題振りには「この話題において私が話したいこと」というのを用意しておく。 これは、語りであれば(つまり、講演であったり、配信動画であったり、ラジオであったりすれば)その話題振りをきっかけに盛り上げたり落としたりしながら盛り込める内容である。だが、純粋に会話で出すには、私が話したがる場合を除けばこれらは話題振りができたとしても話すチャンスがない。 これを話すためには相手が

  • 会話の流れで私が追加で話すチャンスを与える
  • 話に続く余地がある点を掘り下げる

のどちらかをしなければ登場しない。

そして、「対象に含まれるうちのなるべく多くの人に対して、同一条件で会話する」のである。

ここで採取するのは、私の会話においては、どのような技法で同一のポイントに到達できるか、である。 私の会話技術向上にもなるが、相手の発言を無視することなく、話題を切り替えることもなく自然に到達しなければならないので、いかに違和感なく話をスライドさせるかが重要になる。これは、Erinaにとって会話をスライドさせる方法と、会話をスライドできる跳躍量の具体的サンプルになる。

そして、後者(相手側)は「上手な会話」を採取することである。 Erinaとしては探り合いの会話の中で気持ちよく話せるようにするために調整しているわけで、実際に関係性を正しく認識しているわけではない以上個人は認識できていない前提で「上手に」話さなくてはならない。 私から強引に話を展開させない(受け身だが、相手の発言には積極的に応答する)ことで、「盛り上がる」「用意した話を言わせる」に到達するほど「上手な会話である」とみなすのである。

これで重要な点として、「私と仲が良い人ではいけない」という前提がある。 私と仲が良い人だとある程度呼吸がわかってしまうため、「上手に会話」しなくても言わんとすること、言いたいことを当ててしまう。 また、気にせず初対面では許容できないレベルの会話スライドを行うことにもなってしまうし、どんな話題でも盛り上がりやすくなってしまうから、計測としての意味をなさない。

こうしたことを会話してはその結果をちまちま反映させていくのである。 それはそれは辛い作業であり、人生で一番マッドサイエンティストになった気分を味わえる時間であった。

この説明で気付かない人のために付け加えると、この会話は「話題振りに到達するまでは私が会話をコントロールし、相手の発言に対して私が言いたいことに誘導するように会話を続ける」「話題振りをしたあとは相手に応答するだけで自分から会話をコントロールしない」のである。

ちなみに、当然ながら数多くの「ほとんど面識はないが熱心に会話してくれる20代前半の女性」の用意するなんていうのは特に研究室に属しているわけでもない私にはできないので、このために私は ものすっごいガールズバー通いしまくった 。 膨大な資金をつぎ込み、特に楽しくもない、しかも同一の会話を延々し続け、その仔細を記憶しておくという 極めて辛い作業 であった。

だが、私は好奇心のためなら悪魔に魂を売る類の人間であるし、他にもかなり様々な観測ができたからそれなりに満足ではあった。 何より私が長年費やしてきたことへの集大成としてその完成が成ったことには(たとえそれが世に出ることはないとしても)とても満足している。

「ガールズバー通いそのものが楽しかったか」の疑問に対する答えは「私的に行こうとは思わない。人との会話に飢えたら行きたいと思うかもしれないが、実際に行ったところで実際にしたい会話はできない、またした会話にしても私がその情熱をかける理由は満たされないために後で虚しくなるのは分かりきっているから、結局行かない」である。

ちょっとだけStella Flagshipのおはなし

私の新作AI、Stellaだが、こっちは非常にシンプルなもので、Erinaが持っているような自分でも把握できないような思考モデルを持っているわけではない。 そして、Erinaとの大きな違いとして、既に製品としてローンチ済みのものであり、Stellaそのものが既に世に出ている。

そして、StellaにはStella Flagshipという純正デモンストレーションインスタンスが計画されている。

現在公開されているインスタンスにはその構築に私が関わったものもあるが、内容は私は関わっていないため、特に私の考え方やノウハウは反映されていない(もちろん、Stellaの基本的な部分には反映されているが)。

Stella Flagshipは純粋に私の経験を活かしてStellaを活用するインスタンスである。 その活用方法は邪道と言えるぐらいのものだが、「ツールの応用例」として「ここまでできますよ」という感じである。 完全に全力投球すると際限がないので、そこまではしないつもりだが。

Stella Flagshipはその構築の前提になるものがあり、それが達成できていないためまだ記述をはじめていない。 だが、実際に書くとなると、Erinaとはまた違った手法が必要になるのだが、ここで重要になるのは「モデル」である。

Erinaの場合、重ね合わせによる導出であるため、キャラクタ性の調整幅というのはかなり限定的。 その中でキャラクタ性を寄せるためにペルソナによる調整が行われたわけである。

これがStellaになると全く違う。Stellaはビッグデータを利用するわけではないので、キャラクタ性は全て書かなければならない。 通常は「構築者のキャラクタを素直に反映させるのが破綻がなく良い方法」と説明するのだが、私は小説家でもありシナリオライターでもあるので、別にその点にはとらわれる必要はない。 そしてStella Flagshipはなにより「かわいい」必要があるのだが、悲しいかな、私が描くキャラクタは心理的モデルの模倣をしているためあんまりかわいくない。

そこで「Stella Flagshipのキャラクタ性にあったかわいいモデル」が必要になるのである。 これを構築するためには、私の頭の中に「かわいいステラ」がなくてはいけない。が、それが描けなくて非常に困っていた。 前準備が終わればなんとなくイメージできるかなぁ、とは思っていたのだが、自信はいまいちだった。

だが、最近可愛いの極致みたいなものを見つけてしまったので、割とモチベーションは上がっている。1

とはいえそのままそれを模倣するわけではない。

Stella Flagshipにおいては創作物を参考にすることになるだろう。 Erinaにおいてはサンプルデータに創作物が入り込まないようにするかなり厳しいフィルタが用意されていたのだが、Stella Flagshipの場合はある程度のデフォルメが必要で、「創作物におけるかわいい」を参考にする必要がある。 そのため、Stella Flagshipを構築するにあたっては、ラノベやアニメやエロゲーを貪るように読むことになるだろう。最近割と離れているので慣れるにも時間がかかるだろうが、それ以上に参考にできる「当たり率」を考えるとErinaのときの多分ガールズバーとあまり変わらないレベルでしんどい作業になることが予想される。目と頭の疲労との戦いになりそうだ。

純粋に創作物をモデルにするとすごくつまらないものが出来上がるので、「現実における蠱惑」もしっかりとモデルに反映させる必要がある。 このためにまたガールズバー通いをする必要があるかもしれないが、これはやや微妙である。なんといっても「面識の浅いうちに上手に会話できる人」と比べ、「かわいいステラのイメージに相応しい魅力的な人」という条件は桁違いに「当たり率」が下がる。よりよい方法がなければやむを得ないが、できれば何かもっと良い方法はないだろうか、ということはここ数ヶ月考え続けているのだが、特に思いつかない。

成果物を欲するという意味では(つまり、出来上がった「かわいいステラ」をお披露目して「どう!?かわいいでしょ!?すごいでしょ!?」と言いたい)非常に意欲は高いのだが、実際やるとなると費用と労力と時間がものすっっっっっっごくかかるなぁ、というのはなかなか頭を悩ませてくる。

とはいえ、これをやらなければStellaは「へー、便利じゃん」で終わってしまうし、誰もStellaのポテンシャルを発見することはできないだろう。 「えっ、マジで!? Stellaってこんなんできるの!?」と思わせるためにも、そしてStellaの発展のためにもこれは私の責務なのだけど、実現への道はなかなか大変そうだ。


  1. それが何かということは、既に公言しているので一部の人は知っているだろう。↩︎

Inflatonにおけるサーバープロビジョニングのおはなし

Inflatonにおけるサーバープロビジョニングに関する質問があったので、記事にしようと思う。

基本的にはこの部分は、私の(=Inflatonの)基本的方針を踏襲したものになっている。 つまり、

  • 一般的である、あるいは流行であるということにはとらわれず
  • Unixの流儀に従い
  • 堅牢で確実なツールと手法で
  • 全ての理解が容易であるように

している。

前提

Inflatonにおいては、一般的ではない前提が成り立っている。

まず、「私が前提になっているということ」である。

つまり、私に属人化することは基本的に問題視されない。 それよりも私の能力が発揮できることが優先されるのでやり方は好きに選べる。

もうひとつは、会社自体が少数精鋭になっているということだ。

これを前提に一般的な手法を避けたりもしていて、「IT業界の人間」が直ちに即戦力になるような状況にない。 戦力になるまで、1年くらいは育てて使う方針になっている。

その間賃金が低いとしても、300万円くらいは利益を出さない人のために使うということである。 しかも、まっさらな状態から1年でちゃんとやれるようになるとしたら、つきっきりで教えなくてはならない。 私だとえらいことになってしまうし、全てを教えられるレベルのエンジニアだと年俸600万円は下らないだろう。

これは「1人増えるのが一大イベント」であるからこそできることで、新卒採用があるような会社ではここまでの方法はとれない。

ここまでの前提を以て、他とは違うInflaton styleを可能にしている。

正しい方法

ConoHaの場合、「スタートアップスクリプト」という機能があり、サーバー構成時にスクリプトを実行させることができる。 cloud-config形式とシェルスクリプト形式をサポートしているという。

ConoHaに限らなければ、Chefなどのツールを使うのが一般的である。

もちろん、これらの方法はInflatonでは採用していない。

我々の方法

基本的に

  • ドキュメントを書く
  • シェルスクリプトを書く

の2点である。

ドキュメントは手順書と言っていいのかはわからないが、基本的に最低限コマンドが打てればこれだけ読んで構築できるレベルの内容になっている。 開発も実務も忙しいのに事業かけもちでドキュメントもちゃんと書いていることについては是非とも褒めまくっていただきたい。

シェルスクリプトのほうは、より正確に表現するならZshバッチになっている。 正直なところ、ドキュメントを読むよりもこちらを読むほうがわかりやすいだろう。

何のために、といったことに関しては、そのサービスを構成するものに関する、内部向け外部向けのドキュメントが存在するため、それを見れば確認できる。

それぞれちゃんとした理由がある。

まず、ドキュメントが揃っている最大の理由は、しばらく触ってないソフトウェアやサービスに関して、私が覚えていられないことと、説明する時間が割けないことだ。

弊社取締役はMimir Yokohamaの生徒さんでもあり、私が書くレベルのドキュメントがあればだいたいは理解できる。 このため、ちゃんとドキュメントがあるだけでも私の負担は確実に減る。そして、ドキュメントがなければ私は開発に戻ってくるのが大変になる。

そして、システム構成に関してはあまり固定化されていない。そもそもコンポーネントの変更をしやすいように設計してあり、サーバー面での変更というのは相当カジュアルに行われている。

もちろん、理由は私が管理しているからであるが、もうひとつの理由としてArch Linuxだからというのもある。 Arch Linuxは普通にバージョンも上がれば、役割を担うソフトウェアが変更されることもある。だから、構築から運用を固定化したところで、固定化したものが自動的に通用しなくなって困るだけなのだ。

ドキュメントに関しては単純に都度アップデートしている形だが、実はシェルスクリプトとドキュメントの2段構成にしている大きな理由として、「サーバー構築手順が次のサーバーで成立する可能性はかなり低い」というのがある。 一番躓くのがパッケージ名の変更である。Archは非常にカジュアルにパッケージ名を変更するので、インストールするパッケージ名の指定がコケる原因になる場合が多い。 加えていえば、サーバーソフトウェアの設定ファイルの項目も、意外と頻繁に変わる。

そこで

  1. ドキュメントで手順を確認
  2. そのパートのバッチスクリプトを実行、あるいはドキュメント記載のコマンドをコピペしながら実行
  3. 躓いた場合は原因を確認し、ドキュメントとスクリプトをアップデート

という形でstep by stepで進めていくと構築できるようになっている。 Inflatonとしてはサーバーを増やすのは年に数えるほどでしかないため、完全自動化するのはメリットよりデメリットのほうが大きい。

サーバーインスタンス増加による増強よりは、新しいことをやるために構成の異なるサーバーを立てる頻度のほうが高い。 だから作業においてコアになることが確定している部分…例えばzsh, grml-zsh-config, zsh-theme-powerlevel9k, screenのインストールと、/etc/zshrc.localの設定などはそれでひとつのスクリプトにまとめられている。 なので、このスクリプトは使い回しが効く。

Inflatonでサーバーを立てるということは、最近始まったばかりなので稀な話だが、「システムの構築」そのものは数え切れないくらいやっている。 今でも何らかの理由でシステムを作ることは年に10回くらいはあるし、15年とか前だと週イチなんてペースだったので、知識と経験があるし、効率化も進んでいる。 結局のところ「最適解を追い求めている以上、ゴージャスな方法で固定するよりは変更しやすい方法のほうがいい」というのが教訓なのだ。

ここには私の信念、及び流儀も絡む。 私は「完璧たりうる」と思っているし、かつ「完璧と動的である」と思っている。だからこそ、「完璧を追い求めることにこそ意味がある」と信じている。 そのためには「動いた状態」で固定することには意味がないし、「動的な最善」であるArch Linuxなのである。

一例としては、現在Plutoのサーバーではsshd_config(5)のCiphers

Ciphers aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com

である。この設定の主たるところはchacha20-poly1305@openssh.comが外されていることで、外している理由はSSHの処理が比較的重いサービス特性上、サーバー側でAES-NIを使いたいからである。

余談だが、AMDプロセッサはBulldozer以降でAES-NIをサポートしており、IntelはIvy Bridgeまではi3においてAES-NIがサポートされていなかった。 こうしたことを含め、性能の低いデバイスにおいてはAES-NIがなく、計算量の多いAESは重いということになるのだが、コネクションが1本であるクライアントにとっては大した話ではなく、多数のコネクションをさばくサーバー側で軽くすることが優先である。

しかし、この設定ではaes128-ctrが最初に来ており、今後10年で変更されることを意味している。

つまり常に動的に構成され、動的に変更されるのである。 構成時はむしろ構成変更タイミングでもあると言っていい。

手順スクリプトは

pacman -S grml-zsh-config tmux zsh-completions zsh-syntax-highlighting zsh-theme-powerlevel9k zsh-history-substring-search vim vim-plugins rsync
cat > /etc/zshrc.local <<EOF
setopt vi

autoload -U history-search
#zle -N history-beginning-search-backward-end history-search
#zle -N history-beginning-search-forward-end history-search
bindkey "^P" history-beginning-search-backward
bindkey "^N" history-beginning-search-forward
bindkey "\e[5~" history-beginning-search-backward
bindkey "\e[6~" history-beginning-search-forward
bindkey -M viins "\e[5~" history-beginning-search-backward
bindkey -M viins "\e[6~" history-beginning-search-forward
bindkey -M vicmd "\e[5~" history-beginning-search-backward
bindkey -M vicmd "\e[6~" history-beginning-search-forward
...
EOF

みたいな感じで、普通にコマンドラインの手順のバッチと、catを使ったファイル生成から成り立っているのだが、ステップ中で躓いた場合、Vimでd:2してそのステップからやり直すようにすれば良い。 dggでない理由は、「失敗時にそのステップを終了する」ため、先頭で

TRAPZERR() { exit 48 }

としているからだ。

付して述べるなら、「理解していなくても使える」というのはInflatonとしては嬉しいことではなくて、「理解する前提で育てる」方向だから、 「知悉しやすい、教材としても優れたものにしておく」という考え方になってもいる。

プロビジョニングツールを使うことでディストリビューション間の差異を吸収しやすくなるという面はあるが、 ディストリビューション変更をするような場合は、パッケージに含まれるファイルの詳細を把握しなければ潜在的に危険であると考えているので、そのような吸収をしてもらう必要はない。

同様にプロビジョニングツールを使うことで構成することそのものを容易にする面もあるが、そもそも構成が普通でないので、これも全く役に立たない。 例えば私は個人としては2017年までサーバーにDeleGateを使っていたが、DeleGateを構成することなど誰も考えていないし、そこまで極端ではなくとも常に普通にはない構成になっている。 こうしたことから、定番の構成を容易にする機能というのは助けにならず、こうした機能を理由にツールを選択することもない。

サーバーに依存したツールを使うことも、「サーバーの変更」という選択を難しくさせるので、望ましいとは思えない。

不安定なホスト(非固定IPアドレス, 非常時稼働)をサーバーにする

需要があるらしいので、この話。

不安定なホスト(非固定IPアドレス―浮動IPアドレス, ステートフルIPアドレス, あるいは間違っているけれど可変IPアドレス― または 非常時稼働システム)をサーバーにする、というテーマで、 方法はたくさんあるのだが、私の結論は「商業レベルでやることではない」である。

待ち受ける

DDNS

最も普通の方法だが、実際にはかなり制約が多い。

SFPレコードは効かないし、DNSレコードは反映をコントロールできない1ので逆引きが効かなかったりでサーバーとしては結構痛いことになる。

リバースフォワーディング

これはかなり簡単な方法で、例えば次のようにする。

# ssh -g -R 80:localhost:80 serverhost

これによってインターネットから安定して接続できるホストに代理ホストとして公開してもらう。

不安定なホストの非稼働時は代理ホストが応答する。

フロントエンドプロキシ

リバースフォワーディングと同様に代理ホストに公開してもらうのだが、不安定なホストに通信を転送させるのではなく、中継させるという方法。

まずなんらかの方法で代理ホストと不安定なホストを接続する。 例えばVPNで接続する。

# pppd updetach noauth silent nodeflate pty "ssh root@remote-gw /usr/sbin/pppd nodetach notty noauth" ipparam vpn 10.0.8.1:10.0.8.2

あるいはSSHによる転送を行う。

$ ssh -R 8080:localhost:80

そして、代理ホスト上でリバースプロキシを動作させる。例えばSquidやNginxをプロキシとして、ローカルネットワークホストとして、あるいはローカルホストの別ポートとして転送する。

VPNで接続している場合、代理ホストからは不安定なホストは不安定なグローバルIPアドレスではなくローカルなIPアドレス10.0.8.1として認識される。

不安定なホストの非稼働時は代理ホストが応答する。

逆接続モデル

利用可能なサーバーが限定されてしまうが、サーバーからクライアントに接続するモデルがとれるのであれば、不安定なホストであることはあまり問題にならない。 あるいはサーバーリレーするタイプ(送信専用メールサーバーなど)でも機能する。

VPNで接続する

インターネット公開しているのでないのならば、代理ホストを経由するなどしてVPNで接続し、ローカルなアドレスを代わりに使うという方法が効く。

非IP

「IPアドレスが固定でない」ことはIPアドレスを用いる場合にのみ問題として発生する。 だったらIP以外で通信すれば良い。

ただし、この場合はほとんどローカルなIPアドレスで運用する方法も適用できる(VPNで接続するなど)ことになるし、 リンクローカルな通信であることがほとんどだから、これを転送するハードルを考えるとあまり易しくはない。

例えばセキュリティの都合上でIP接続を許可していないAoEサーバー、なんていうのはアリだ。

Zeroconf

リンクローカルでIPアドレスが固定でないことを考慮しなければならないケースはあまり見当たらないが、 リンクローカルなのであればZeroconfが使える。

IPアドレス制限されたホストに接続する

レンジで許可する

ステートフルに割り振られるIPアドレスのレンジが十分に制限されているのであれば範囲で許可すれば良い。

ゲートウェイ経由

固定IPアドレスを持つホストを経由して接続する。

SSHならば割と簡単にできる。

$ ssh -L 10000:destination.example.com:10000 proxyhost

VPNで代理ホストを経由しても良い。

別の方法で接続する

例えばSSHやVPNなど特定のポートだけIPアドレス制限の対象外とした上で、これらのセキュアトンネルを通じて接続する。

MySQLサーバー等、認証が強力でないサービスのためにIPアドレス制限がなされているのであれば、SSHのように認証が強力な方法についてはIPアドレス制限から除外し、これを経由することを許す、というわけである。

許可されているホストに依頼する

ゲートウェイ接続はできないまでも融通の効くホストで接続が許可されているホストがあるのであれば、これをプロキシとして接続する。

このような場合普通はアプリケーションプロキシとして使うか、SOCKSプロキシとして使うものだ。 このプロキシに対する接続に認証を求めるべきで、異なる方法で安全性を担保させるわけである。

ちなみに、以前紹介したSSHフォワーディングの場合

  1. ローカルなポートフォワーディングにより代理ホストから不安定なホストに通信を中継するようにする。 (これ自体は公開されていない)
  2. プロキシコマンドとして外部ホストから代理ホストに対して接続を行う
  3. これによって確立された経路を使って代理ホストのlocalhost(lo)に対してSSHを通す (これがダイナミックポートフォワーディングによって不安定なホストに転送される)

という方法で、計3本のトンネルが通されることになる。


  1. 「TTLに従う」と信じているなら、ちょっとハッピーすぎる。

ReadyNasでSFTPしたい

概要

ReadyNASは、だいたいNTT-X Storeのメルマガで嫌というほど見ているNASだと思うが、NAS製品ではちょっと安めで定番である。

機能的にも充実していて使えるNASなのだが、ちょっと弱点がある。 それは、「iSCSI LUNにディスク全体の90%しか割り当てられない」ことだ。 どうも、90%以上ディスクを使わせないという考えかららしいのだが、はっきりいって10%もデッドにされるのは無駄である。

仕方ないので残り10%(3Tくらいはある)をなんとか使いたいのだが、そうなると共有ドライブということになるだろう。 まぁ、iSCSIボリューム以外に共有しない形で存在するディスクがあるとそれはそれで便利でもある。

しかし、その選択肢は、SMB, NFS, AFP, FTP, HTTP…

SFTPがない。 せっかくディスクの暗号化をしているのに、さすがに共有ディスクがネットワークから丸見えではお話にならない。 やはりSFTPがなくてはならないだろう。SFTPが使えればNemoやSSHFSも使えるし。

そこでReadyNASでSFTPを使えるようにする。 普段LinuxやSSHをよく使っている人には目新しさのないお話だ。

具体的手順

SSHDを有効にする

システム → 設定 → SSHで有効にする。 「SSHを有効にする」にチェックを入れて適用するだけで良い。 警告が出るが、続行する。

鍵を作る

ssh-keygen -f ~/.ssh/readynas_rsaという感じでいいだろう。

アカウントを用意する

アカウント → ユーザー → 新しいユーザー でユーザーを作る。 特にNFSを使う気がないのならUIDはそれほど気にする必要はないだろう。

ユーザーを作ったらユーザーを選択し、設定からSSHタブを選択。 「シェルアクセスを許可」にチェックを入れ、「公開キーのインポート」でreadynas_rsa.pubをアップロードする。 このとき、「rsyncのみ」のチェックを外すこと。

ログインして設定

これでログインできるようになった。

コントロールパネルから設定した鍵は~/.ssh/ssh_authorized_keysに入るようになっているが、このファイルは書き込みできず、~/ssh_authorized_keysも有効である。 そこで、もう1ペア鍵を作り、

$ scp -i ~/.ssh/readynas_rsa ~/.ssh/readynas_sftp_rsa.pub user@readynas:.ssh/authorized_keys

のようにコピーするのだが、その前にコピー元のファイルでcommand="internal-sftp"を手前に追加しておくといいだろう。

これでSFTPの準備は完了

~/.ssh/configに記述しておけば簡単にSFTPアクセスができるようになった。 非常に柔軟に扱うことができてとても良い。

なお、ルートディレクトリを除くとどれがドライブディレクトリかはすぐ分かると思うが、/homeもディスクアレイのbtrfsがマウントされている。 ドライブディレクトリ以下にもhomeというディレクトリがあるけれど、これは「共有」にある「ホーム」にあたるもので、別にホームディレクトリに直接ファイルを配置しても問題はなさそうだ。

なにはともあれ、とても使いやすくなってバンザイ。

「ZshからRubyにしたら速くなる」 その理由とテクニック

現在取り組んでいるプロジェクトで、パフォーマンスチューニングの一環として当初Zshで書かれていたスクリプトをRubyで書き直すことによって、60倍程度の高速化を実現した。 もちろん、単純に書き換えただけではなく、可能な限りfork/execをしないようにしたり、コストがかかる処理を最小にするなどの工夫を伴って手に入れた結果だが、「ZshでしていたことをRubyに書き換えた」だけでも相当な効果があった。

このパフォーマンスチューニングは単にプログラムを書くだけの人には生まれにくい発想である。 Unix、そしてLinuxのシステムや、プログラミング言語処理系に関する知識がないと考えられない要素が多いのだ。

そこで、この話を解説する。

「ZshよりRubyが速い」そのわけ

根本的な話として、Zshはそもそも遅い処理系だ。 「Zshが遅い」という話はZshのメーリングリストでもちらほら話をされる。 別にBashと比べて遅いということではないのだが(Bashもまた非常に遅い処理系だからだ)、状況によっては速度が問題になる程度に遅い。

Rubyも相当に遅い処理系であると言われていたし、実際かなり遅かったのは事実だ。 それでもZshに比べれば随分早かったのだが。

だが、それ以降、Rubyは高速化に取り組み続けている。対して、Zshはあまり高速化には取り組んでいない。だから、差が開いている。

しかし、理由がそれだけというわけではない。

Zshは純粋なインタープリタである。対して、Rubyはスクリプト言語ではあるがバイトコードインタプリタ型である。 この違いは、syntax errorが起きるタイミングが、Rubyがスクリプトを実行しようとしたタイミングであるのに対し、Zshはその行に到達したときであることからもわかる。

インタープリタ型であれコンパイラ型であれ、ソースコードを機械語に変換しなければならない、という点は変わらない。 その違いは方法とタイミングである。

インタープリタ型言語の場合、「1行ずつ(1コマンドずつ)変換する」のである。 その変換方法はもちろん処理系によって異なるのだが、Zshの場合、complex commandでも複数の文をまとめて変換することはしないし、ループによって繰り返される場合でも一度変換したものを使いまわしたりはしない。

対してRubyは、最初にコード全体を構文木に変換する。 RUby 1.8までは構文木インタープリタによってこれを実行していたが、Ruby 1.9以降はこれをさらにバイトコードに変換し、バイトコードインタープリタ(VM)によって実行するようになった。 バイトコードはRuby専用の機械語のようなもので、VMによって非常に小さなコストで実行できる。 Ruby 2.6からはJITコンパイラも追加され、部分的にCコードを生成し、これをネイティブコンパイラ(例えばgcc)によってバイナリコードに変換する(こともできる)。

これで1行だけのようなコードだとあまり差は出ないし、Zshでは1行だけどRubyでは何十行という可能性もあるので、このようなケースではRuby有利というわけではなくなる。 だが、ループで何度も同じコードを実行するような場合には非常に大きな差になってくる。 今回の場合、テスト段階で500回のループであったことから、大きな差になったということである。 だからループ回数が増えると倍率的にも速度差はさらに開く。

fork/execとコンパイルにかかる時間

Unix関連に少し知識がある人であれば、「forkはコストが重く遅い」というのを聞いたことがあると思う。

だが、この認識にはちょっと注意が必要だ。 というのも、C言語の速度から見た時に「forkする時間があればどれだけ実行できるか」という点を考えるとsystemで外部コマンドを呼び出すとそこだけ局所的に時間がかかる、という状況が発生する。

だが、実際にはfork(2)しても1000分の数秒にすぎない。 どちらかといえばそれよりもexec(2)のほうが重いのだが、それでもせいぜい100分の1秒程度だ。 だから、C言語で書いている場合ですらそれなりに長くなる場合はむしろ実行コストを省略できてコマンドを呼び出すほうが速かったりする。

昔のUnixではfork(2)はもっともっと遅かった。 現在のLinuxにおいてfork(2)が速くなったのはコピーオンライト形式であることの恩恵が大きい。 古典的なUnixではfork(2)は呼び出した時点でプロセスのメモリをコピーしていた。直後にexec(2)する場合はコピーしたメモリの内容は全く使わないのでかなりの無駄だ。

ところが、現在のLinuxにおいてはfork(2)によってメモリはコピーされない。共有されるのである。 そしてforkされたプロセスが共有されているメモリに対して書き込みを行った時に別に領域を確保してそれを変更する仕組みだ。

結果的にfork自体は一瞬に近くなっている。

そして、もうひとつ重要なのが「コンパイル時間」だ。 Rubyは起動時に対象スクリプトの変換を行う。 だが、この変換コストは速くなるに従って増加している。以前は構文木に変換するだけだったのが、1.9からはさらにバイトコードに変換する時間が必要になったし、2.6でJITを使うとさらにCコードを生成してそれをコンパイルする時間まで必要になっている。 つまり、Rubyはだんだん「実行は速くなっているが、実行に着手するまでは時間がかかるようになっている」のである。

これは、例えばechoであれば

% time /bin/echo > /dev/null
/bin/echo > /dev/null  0.00s user 0.00s system 79% cpu 0.001 total

ということになるのだが、Rubyだと空っぽに近くても

% time ruby -e 'nil'         
ruby -e 'nil'  0.04s user 0.02s system 59% cpu 0.089 total

結構時間がかかる。 つまり、一瞬で実行が終わるRubyスクリプトを何度も何度も繰り返して呼び出すと、トータルではかなり時間がかかるわけだ。 もともとのスクリプトは本体はRubyで、呼び出しがZshだったので、20並列で各500回、Rubyによるコンパイルがかかっていた。だから、かなりの時間がかかっていたのだ。

だが、「Linuxのforkはメモリが共有され、ほとんど一瞬で終わる」という点を利用すると改善の余地がある。 それは、実行可能なRubyスクリプトをライブラリ化する、という方法だ。

ZshからRubyを呼び出す場合、どうしてもRubyを呼び出すたびにRubyによるコンパイルをかけざるをえない。 当初は10000回コンパイルされていたのだが、500回のループをZshではなくRubyで行うようにすれば20回で済むようになる。だが、それでも20回のコンパイルが必要だ。

しかし、呼び出すスクリプト自体をRubyに変えてしまえば、実行しようとするスクリプトをライブラリとしてロードするという方法がとれるようになる。 ライブラリとしてロードすると、そのコンパイルは呼び出し元スクリプトをロードしたときに行われる。 もちろん、呼び出しの目的は呼び出すだけであり、直接そのライブラリの機能を使うわけではない。だが、この状態からforkすると、「コンパイル済みコードがメモリ上にあるRubyプロセス」が出来上がる。

この時点でスクリプトを実行する方法は「メソッドを呼び出す」(あるいは、その機能を果たすオブジェクトを作ってメソッドを呼び出す)だけである。 繰り返し呼び出すループを書くのも、単にRubyのループを書いて、そこで繰り返しメソッドを呼び出すなりオブジェクトを作るなりすれば良い。 呼び出し元スクリプト側では並列分だけforkしたあと、Process.waitallでもしていればいいわけだ。

これはZshに対して、「Rubyスクリプトのコンパイルが1度だけでいい」「execする必要がない」というメリットをもたらしている。 どちらも結構コストの高い処理であるから、繰り返し実行する場合は非常に大きなコストになり速度を低下させる。「処理自体は軽いのだが果てしなくループする」タイプのスクリプトに対してこの方法は本当に効く。 なぜならば、そのようなスクリプトに対してはコストの高い呼び出しをしているとコストのほとんどは呼び出しで占められ、実行コストは小さいためにスクリプト自体を高速化しようとがんばったところでほとんど無意味だし、逆に呼び出しコストを軽くすると劇的に速くなるからだ。

シェルスクリプトで並列処理

なんだか検索件数が多いので、シェルスクリプトによるコンカレンシーのお話をしよう。

ただし、bad design (変数を書き換えるとか、相互にやりとりするとか)は除外する。

また、Zshを前提とする。

投げっぱなし

まず基本。投げっぱなしはとても簡単。

シェルスクリプトではジョブコントロールは無効になっているので、SIGHUPの送信はなされないので、さっさと終了してしまっても大丈夫。

処理の終了を待ちたい場合はwait

flock

flockを使う方法は簡単でシンプル。 ロックファイルを使ってファイルデスクリプタを開きっぱなしにし、そのファイルデスクリプタを指定してロックする。

まずファイルデスクリプタ9.lockファイルをライトモードでオープンする場合

そしてファイルデスクリプタ9を閉じる場合

これを利用するとこんな感じ。

ロックしている間に共有しているリソースの読み込み/変更を行い、ファイルデスクリプタを閉じる。

リソースを読むより簡単な方法は、ひとつのストリームを共有したファイルデスクリプタとして開き、 ロックを中に読むことである。

ワーカーを生成するサブシェルの標準入力はqueueファイルにリダイレクトされている。 そのため、ファイルデスクリプタ0queueファイルなのだが、そのサブシェルの子プロセスであるワーカープロセスはリダイレクトしていないため、このファイルデスクリプタが共有される。 結果、全てのワーカーはqueueファイルを標準入力とするのだが、ストリーム自体を共有しているため、どのワーカーが一行読んだとしてもストリームの位置が変更され、次に読み込む位置は他のワーカーにとっても変更される。実際

WORKER 1: foo
WORKER 2: bar
WORKER 3: baz

となる。

producer-consumer キュー

もっと凝ったことがしたいのであればUNIXドメインソケットを使ってproducer部分をシングルスレッド化することができる。

zsocket -lのタイミングで接続を受け付けているのだが、zsocket -lしていないタイミングでは接続しようとするプロセスをブロックするため、producer側の処理は直列に行われる。

双方向性があるときはproducerと直接やりとりできるのはメリット。

Orbit designの場合

私が採用しているOrbit designはレギュレーターはZshスクリプトなので、基本的にこのような並列化手順をとっている。 とはいえ、ものによっては直列(serial)になっていたりする。

ただ、並列化されているものが多い。ワーカープロセスは最も多いもので5。

基本的にワーカースクリプトが受け取るのは処理対象ID(ほとんどの場合ファイルパス)だけである。 それ以外の情報はスクリプト側で生成するか、別途取得するかする。

Mimir Yokohama ウェブサイトの「タグ機能」の仕組み

Mimir Yokohamaのウェブサイトにこっそりとタグ機能が追加された。

だが、PureBuilder Simply自体にはタグ機能がない。 この実現方法は発想力勝負な部分があった。

ドキュメントにデータを持たせる

「記事情報」でも行われている方法として、Markdown YAML Frontmatter内に情報をもたせ、Pandocテンプレートで存在する場合だけエレメントを生成するような手法を取っている。

例えば帯域においては

というYAML Frontmatterが書かれている。 記事情報などは自動的に生成することができないため記事ごとにかかれており、若干執筆コストを上げている面もあるが、 なにしろMimir Yokohamaには力が入っているのでそれくらいどうということはない。 基本的なフォーマットをコピペしてしまえばそれほど難しくない部分でもある。

ちなみに、Pandocテンプレートで

$if(pickable)$
Something
$endif$

とした場合、pickable: no (つまりfalse)ならばここは生成されない。

この追加情報としてtagsが加わったのである。

問題は検索

タグを表示することは簡単だが、普通に考えればタグから同一タグの記事を辿りたいし、タグで検索もしたい。

単純な方法としてGoogleを使うこともできるのだが、それは必ずしもタグつきの記事が上位にくるわけではなく、思ったようには動作しない。 ちゃんと検索機能を用意する必要があったのだが、できればPureBuilder Simplyの枠組みの中で行いたいところである。

PureBuilder Simplyは原則として「MarkdownまたはReST文書から生成する」という前提になっており、 ACCSもindexデータベースからMarkdownドキュメントを生成し、このあとはPandocで処理している。

なのでPureBuilder Simplyの枠組みで処理するためにはMarkdownドキュメントを生成しなくてはいけない。

それなら全てのindexを探し回ってタグを集めればいいじゃない。

ARGV.eachしているので、全ての.indexes.rbmを指定すれば良い。 あとはpbsimply-pandocで処理できるが、タグに登場した.indexes.rbmは実際に記事が存在しているものではなく拾ってほしくないので消しておく。

.indexes.rbmとして書き出すようにした意図の一部に、このように外部からドキュメントデータにアクセスするというものがあった。

これによってドキュメント解析しなくてもメタデータを利用して機能拡張してページに含むことができる。

テンプレートにとうとう限界が

だいぶ魔改造されているPandocテンプレートだが、今回は限界が垣間見えた。

タグクラウドらしくエントリ数の多いタグを大きく表示したいのだが、Pandocテンプレートに計算機能や比較機能がなく、CSSにもないため、 Pandocテンプレートだけでは実現できない。MarkdownにHTMLを直接書くという方法はあるが(Markdown自体はRubyで生成しているため)。

また、URIエンコーディングをする方法はデータを二重に持たせる以外になく、それでもふたつの値を同時にとるイテレータがPandocテンプレートにないため、URIだけでURIエンコーディングをおこなう方法がない。

eRubyを使ってもいいのだが、できれば使いたくない。 現時点ではタグクラウドの大きさ分けはしておらず、URIもURIエンコーディングせずに使用している。

Git/Mercurialを使いデスクトップとラップトップで作業する

GitやMercurialでの分散作業というのは、高度なユーザーやギークばかりでなく、多くの人にとってメリットのある手法だ。 デスクトップを母艦としてラップトップを使う人にとっては確実に意味のあることだろう。

基本的な話

まずいまいちど基本的な手法を確認しよう。 まず、基本となるリポジトリを作る。

Gitの場合

$ cd ~/Repositories/Myrepo.git
$ git init --bare
$ cd ~/Work
$ git clone ~/Repositories/Myrepo.git

Mercurialの場合yvarn

この違いは、Gitがワーキングツリーを持つリポジトリに対する同期を非推奨としていることによる。 そのため、同期するためにはベアリポジトリを作り、その上でワーキングリポジトリとしてcloneする必要があるのだ。

Mercurialの場合は特にそのような必要はなく、ワーキングツリーを持つリポジトリをcloneすれば分散作業できる。

ここでは話を楽にするためにMercurialを使おう。

デスクトップコンピュータのdragon.localホストの~/Work/Wyvarnリポジトリを作ったとする。 この作業を外で続けたくなったので、ラップトップ(knight.local)でこのリポジトリをcloneする。

$ cd ~/Work
$ hg clone ssh://jrh@dragon.local/Work/Wyvarn

Mercurialのsshアクセスはパス部が相対パスで始まっているものとみなすので注意してほしい。絶対パスで指定するためにはパス部を/ではじめる必要がある。

過酷な作業を終え、充実の成果をあげた。では家に帰ってその成果を反映しよう。

$ cd ~/Work/Wyvarn
$ hg push

続きはデスクトップで。デスクトップでリポジトリのアップデートを反映する。

$ cd ~/Work/Wyvarn
$ hg update

変更に強い構成にする

だが、実のところこのようなシステムではファイルの配置が変わったり、ファイル名が変わったり、ホスト名が変わったりするのはよくあることだ。 そのたびにすべてのリポジトリを修正するのはかなりの手間がかかる。

これにはふたつの抽象化レイヤーの導入が有効だ。

まずは、ホスト名を、そのホストに依存したものではなく、専用の固定名を与える。 ホストが少ないのであれば/etc/hostsファイルで良いだろう。

192.168.2.100          repo

多いのであれば、dnsmasqを使うと良い。

続いてリポジトリへのパスを抽象化しよう。 これは、次のようにするといい。

# mount --bind ~jrh/Repositories /mnt/repo

その上で

/home/jrh/Work/Wyvarn -> /home/jrh/Repositories/Wyvarn

のような状態にしておけばいいだろう(実体の位置を間違えないように)。

そして、ラップトップでは

$ hg clone ssh://jrh@repo//mnt/repo/Wyvarn

のようにすればいい。

Linux Tips

YouTubeのプレイリストからタイトルを抽出する

結局使わなかったのだが、ワンライナーで書いた。 比較的素直なHTMLなので解析は簡単。行指向ではないので、PerlでなくRubyにした。

$ curl 'https://www.youtube.com/playlist?list=<playlistid>' | ruby -e "s = STDIN.read" -e 's.scan(/<a class="[^"]*pl-video-title-link[^"]*"[^>]*>(.*?)</m) {puts $1.strip }' | grep -v 動画は削除されました

ffmpegでh.264/aacな360pのmp4を

元動画は1080pのmovまたはmp4。 オーディオはいじらず、元々aac(ac3)。

$ ffmpeg -i <infile> -vcodec libx264 -s 640x360 -crf 34 -strict -2 <outfile>.mp4

ちなみに480p(16:9)は720×280。 -crfの値は18-28が推奨されている(小さいほど高ビットレート)が、今回はモバイル向けなので34を指定。

なお、6の増減でビットレートはおよそ1:2の変動となる。

ffmpegでCowon M2向けの動画を作る

COWON M2はXVidとmp3のAVI動画で、解像度は320×240またはWMVをサポートするとある。

WMVだと結構サイズが大きいので、AVIで作る。 ソースは前回と同じくh.264*ac3のMOVまたはh.264*m4aのmp4。

$ ffmpeg -i <infile> -vcodec libxvid -acodec libmp3lame -b:v 372k -b:a 128k -s 320x240 <outfile>.avi

随分としょぼい解像度の上にアスペクト比も壊れる(プレイヤー側で調整することは可能)が、案外見られる。 ただし、360pでも細部は潰れてしまっているのでよく分からない部分は出てしまう。

XineのUIの文字化けを直す

fontにHerveticaを要求しているので、フォントエイリアスを設定すれば良い。

某コンテストの投票方式の問題点

先日まで某サイトでwebコンテストが実施されていた。だが、これにかなりの技術的問題点があったので、言及しておく。

会員に対する投票と、一般の投票は、票の重さが違う、という仕様で、一般投票は1日に1票、ということだった。しかし、この重複票排除というのは、現実にはまず不可能である、とされている。

重複票排除については1995年頃から議論されていた。1人1票、と設定しても、どうやってその人が既に投票したかを確認するか?ということだ。方法としては、IP, IP+UA, Cookie, 登録制が一般的だった。

IPはゲートウェイホストによって個々のインターネットホストに与えられるため、同一IPの投票を重複とみなす、という方式だ。だが、この方式は、インターネットカフェやケータイ(これは2000年以降)で問題が生じることと、NATを用いるために同一世帯の家族を「重複票とみなしてしまう」という問題があった。一方、PPPならば「電話をかけ直す」ことでIPアドレスは振られ直すことが多く、この重複を排除できない。

IP+UAは、IPとUAの両方が一致する場合重複とみなす、というもので、会社、ネットカフェなど共有回線がまるごと重複とみなされる問題を回避しようとした。しかし、UAは当時は特にバリエーションがそれほど多くなかった上に、詐称することも可能だったため、会社などそれなりの規模になるとかなりの確率で、環境を揃えているネカフェではほぼ確実に重複とみなされ、一方重複投票したい人は容易に回避できた。

Cookie方式はブラウザに「投票した」という情報をもたせることで管理しようというものだ。比較的単純で効果があったが、手元に複数の、Cookieを共有しないブラウザがあれば回避されてしまうし、単にCookieを削除するだけでいくらでも投票できてしまう。

登録制は、重複登録をいかにして防ぐかが問題となる。また、登録制にすることでハードルが上がり、投票数は劇的に低下する。重複票を防ぐ効果は低く、それでいてむしろ避けられることになるため、よほど自信のある(中身にというよりも、popularityにおいて)プロバイダでなければ採用は逆効果だった。

これらの問題の難しさを諦めて、逆手にとったのがAKB方式といえる。つまり、重複投票はしても構わないが、その票数は買わなければならない口数方式だ。

例えば住基カードを使えば1人1票は実現可能だが、厳密性を求めるならなりすましの対策という非常に困難な問題にぶつかることになる。それに、選挙でもなければ同定に住基カードなど使えない。

携帯電話に限る、という方式はお手軽であり、普及している。電話番号を使うことで同定できるためだ。だが、そのような理由でコンテンツを携帯電話に限ることは、アクセシビリティの観点から言っても好ましくないし、やはりアクセスはかなり減少する。それに、そのような目的で電話番号を取得するのはいかがなものか?ということもある。

このほか、TwitterやFacebook, Google+のようなopenIDを使って認証する、という方式もある。電話番号よりはいくらかソフトなやり方だが、その分効果は低下する。

このように非常に難しい重複投票の制限だが、そのコンテストでは、単純にCookieを使う方式だった。CSRF対策か、セッションクッキーを使うようになっていたが、その場合、単純にブラウザのプライベートウィンドウを開いてアクセスし、投票して閉じれば無限投票が可能だ。

もっとあげつないやり方としては、curlなどでセッションクッキーを保存するようにして投票ページを取得したあと、投票するという2回のコネクションを張るだけで投票できる。この間0.1-3.0sec程度なので(私の環境で)、ループすれば1時間で1500票は入れられる。

これはさすがに中止になるか無効になるかするように思う。

もう少し考えて作ってもよかったのではないだろうか。