4 ポイント 投稿者 GN⁺ 2024-05-27 | 1件のコメント | WhatsAppで共有
  • 拡散モデルは、データにノイズを段階的に混ぜてから元に戻す方法を学習し、Stable Diffusionのような画像生成だけでなく、動画・3D・タンパク質構造・ロボット軌道生成まで活用されている
  • DDPMはクリーンな画像 (x_0) を複数の timestep にわたって (x_T) に近いノイズへと変換し、モデルが各段階のノイズの方向を予測するよう L2 損失で学習する
  • 逆過程は小さなノイズ段階ではおおむねガウス分布として扱うことができ、Ho 2020 は分散を固定して学習を安定化し、平均予測をノイズ予測へと単純化した
  • 生成速度は、DDPMの最大1000ステップのサンプリングから、ODEベースのサンプラーで約10ステップ、progressive distillation と adversarial distillation による単一ステップ生成まで短縮できる
  • 拡散モデルの実用範囲は広がったが、過度な distillation による品質低下、条件制御の限界、データセットの倫理と出所の問題は引き続き制約として残っている

生成モデリングにおける拡散モデルの位置づけ

  • 生成モデリングの基本問題は、未知の分布 (p(x)) から得られたサンプル集合があるとき、同じ分布から新しいサンプルを生成することである
  • GANは生成器と判別器のゲームとしてアプローチし、印象的な画像を合成できるが、学習が難しく、(p(x)) を明示的にモデル化せず、mode collapse が生じる可能性がある
  • Normalizing flow は、データサンプルと単位ガウス分布の間の決定論的・可逆的なマッピングを学習する
    • OpenAIの2018年のGlowは、意味のある latent space を持つ顔画像を生成した
    • flow は各レイヤーが可逆でなければならず、Jacobian determinant を高速に計算する必要があり、任意の (N \times N) Jacobian determinant の計算は (O(N^3)) のため制約が大きい
  • 拡散モデルは決定論的マッピングの代わりに、データへランダムノイズを少しずつ混ぜる確率的マッピングを複数段階で実行し、その過程を逆にたどるモデルを学習する

Denoising Diffusion の基本アイデア

  • クリーンなデータポイントに小さなノイズを何度も混ぜると、最終的にはほぼ純粋なノイズのように見える状態になる
  • ある時点のノイズが混ざったデータポイントだけを見ても、前段階でどの方向から来たかをある程度推定できる
  • 十分に多くのノイズを混ぜると、target space の一点 (y) が元の空間の (x) から来た確率は (p(x)) に任意に近づけることができる
  • 複数段階の逆過程を学習すれば、(p(x)) からサンプリングできる
  • 学習では、各データポイントにランダムノイズを加え、モデルにそのノイズを予測させたあと、予測ノイズと実際のノイズ方向の間のL2 lossを gradient descent で最小化する
  • 基本的なサンプリングは次の流れに従う
    • 純粋なノイズ画像から始める
    • 画像内のノイズを予測し、定められた比率だけ引く
    • sampler に応じて 10〜1000 回繰り返し、ノイズのない画像を得る

DDPM: 拡散モデルの数学的出発点

  • DDPM(Denoising Diffusion Probabilistic Models) は、その後の拡散モデル発展の言語と数学的基盤になったアプローチである
  • 入力画像 (x_0) は (t=1,2,\dots,T) timestep の間、小さなランダムノイズが繰り返し混ぜられ、単位正規分布上の点へと移動する
  • forward diffusion step は、前の画像 (x_{t-1}) にノイズ (\epsilon \sim \mathcal{N}(0,I)) を混ぜて (x_t) を作る
    • (\alpha_t) は 1 より小さいが 1 に近い
    • (\prod_{t=1}^T \alpha_t \approx 0) となるよう設定される
    • 平方根項は各段階の後でも分散が維持されるようにする
  • 各段階は直前の timestep にのみ依存し、混ざるノイズは以前のノイズと独立しているため、(x_t) は (x_0) と1つのガウスノイズベクトルで直接表現できる
  • この性質は、逆過程とノイズ予測学習目標を導くために用いられる

逆過程とノイズ予測学習

  • noisy image (x_t) と元画像 (x_0) が分かっていれば、前のより noisy でないバージョン (x_{t-1}) の分布はガウス分布として閉じた形を持つ
  • 生成時点では元の (x_0) が分からないため、(q(x_{t-1} \mid x_t)) を直接使いたいが、生成時にデータセット全体を使うことはできない
    • データセットは数十億枚の画像である可能性がある
    • すべての timestep でこれに従うと、新しいサンプルではなく学習サンプルを復元する結果になる
  • (q(x_{t-1} \mid x_t)) は非常に小さなノイズ段階ではおおむねガウス分布であり、これは統計物理学の古典的な結果である
  • モデルはガウス分布のパラメータ (\mu_\theta, \Sigma_\theta) を推定し、各学習例の既知の分布 (q(x_{t-1} \mid x_t, x_0)) との KL divergence を減らす
  • Ho 2020 は (\Sigma_\theta) を (\Sigma(t)) と等しく固定した
    • 分散を学習しようとすると、学習があまりにも不安定になった
    • 実際には平均 (\mu_\theta) だけを学習する
  • 平均予測問題は、(x_t) が (x_0) とガウスノイズ (\epsilon) の混合として表現できる点を利用して、ノイズ予測問題へと変換される
  • 最終的な単純損失 (L_\text{simple}) は、実際のノイズ (\epsilon) とモデル予測 (\epsilon_\theta) の間の二乗誤差を最小化する
    • weighting を無視すると結果品質が向上する
    • これは低い (t) の loss term の重みを小さくし、ネットワークがノイズの多い画像を denoise するより難しい問題に集中するようにするものと見なせる

DDPM サンプリングと 2D の例

  • 学習済みノイズ推定モデル (\epsilon_\theta(x_t,t)) があれば、逆過程をサンプリングできる
    • (x_T \sim \mathcal{N}(0,I)) としてランダムノイズ画像をサンプリングする
    • (T) から 1 まで timestep を逆順にたどりながらノイズを予測する
    • 予測した平均と分散から (x_{t-1}) をサンプリングする
  • DDPM の流れは次の通りである
    • 画像データセットの underlying distribution を学習しようとする
    • forward noising process で (x_0) を (x_T) へと段階的に変える
    • 逆過程は (T) が大きいとおおむねガウス分布として扱える
    • (x_0) がデータセット内の特定の画像だと仮定すれば、既知の分布を正確に計算できる
    • KL divergence により、すべての学習画像についてこの既知の分布に近づくよう学習する
    • この過程は、データセットを再生成する尤度を最大化する lower bound を最適化する
  • 142 個の点からなる Datasaurus データセットの例は、2D 分布学習を示している
    • 3 つの hidden layer、hidden size 64、ReLU を使う MLP は 12,000 個以上のパラメータを持つ
    • timestep 情報を与えないと、望ましい恐竜形状の分布は得られない
    • timestep (t=0,\dots,50) を 0〜1 の間に正規化して入力すると結果が改善する
    • Fourier encoding によって入力を random frequency feature 空間へ変換すると、より良い分布を学習する
  • noising schedule の調整も性能に重要である
    • Ho 2020 の方式は (\alpha_t) を線形に減少させ、(\bar\alpha_T \approx 0) となるようにして高解像度画像に適している
    • 低次元データは中程度まで進んだ時点ですでにノイズのように見えるため、より多くの high-signal な例で学習するよう schedule を調整すると性能が改善する
    • 例の元の schedule は (\bar\alpha_T \approx 0.28) で純粋ノイズまでは至らず、新しい schedule は既存 schedule の中間値である 0.6 で終了する

生成速度を高める発展

  • Score matchingとODEベースのsampler

    • 拡散モデルの初期の欠点は、DDPMの逆分布ガウス仮定のため 生成速度 が遅いことだった
    • モデルが noisy input (x_t) から予測するノイズの方向は、timestep による定数を除けば、forward process が (x_t) を生成する log-likelihood gradient、すなわち score と同じである
    • score は、分布の mode 側へ移動すべき方向を示す vector field とみなせる
    • Noise-conditioning score network は、段階的に noised されたデータセットの score を学習し、score field に沿って新しいサンプルを生成する
    • forward diffusion process は stochastic differential equation(SDE)として記述できる
    • 同じ timestep ごとの分布を持つ deterministic process を記述する ODE も存在し、この ODE は score function を含む閉形式を持つ
    • これにより、事前学習済み diffusion model でも完全に deterministic なサンプリングが可能になり、汎用 ODE solver をサンプリングに使用できる
    • DDPM は、Stable Diffusion で高品質な結果を得るために最大 1000 ステップが必要になる場合がある
    • Euler method ベースの sampler は、およそ 10 ステップだけでも高品質な結果を出せる
    • Karras 2022 は、sampler の tradeoff と、DDPM のような stochastic sampler の stochasticity が一部のケースで重要になりうることを扱っている
  • Distillation

    • Progressive distillation は、1000 ステップでサンプリングする事前学習済み teacher を用いて、student が teacher の 2 ステップ出力を 1 ステップで予測するよう学習させる
    • この過程を繰り返して student を新たな teacher とすれば、ステップ数を毎回半分に減らせる
    • Progressive distillation には損失があり、適用しすぎるとサンプルがぼやけたり非現実的になったりすることがある
    • Adversarial distillation は discriminator を同時に学習して student sample の現実感を高めるが、GAN で見られるように sample diversity との tradeoff がある
    • Stable Diffusion XL Turbo はこの方式で学習され、高品質な画像を 単一ステップ で生成できる

条件付き生成とGuidance

  • 条件付き拡散モデル

    • 動物画像で学習したモデルで猫だけを生成するには、条件付き分布 (p(x \mid y)) をモデリングする必要がある
    • diffusion model (\epsilon_\theta(x_t,t,y)) をデータセットの ((x_0,y)) ペアで学習させれば、class label、text embedding、segmentation mask のような条件情報を利用できる
    • Ho 2021 は ImageNet で class-conditional diffusion model を学習した
    • 特定の (y) に対して (p(x \mid y)) のサンプルが十分でない場合、label が非現実的なサンプルや多様性の低いサンプルにつながることがある
    • そのため、生成中にモデルが label にどの程度従うかを調整する guidance が必要になる
  • Classifier guidance

    • classifier が (p_\phi(y \mid x_0)) を出力する場合、入力に対する gradient を用いて画像を望ましい class (y) の方向へ押し進められる
    • サンプリングの各ステップで classifier gradient を estimated mean に加えると、diffusion process が画像をもっともらしい image space の領域に配置できる
    • classifier は noisy image (x_t) を扱えるよう、noisy image で学習する
    • Fashion-MNIST の「T-shirt」class の例では、精度 40% の classifier でも classifier-guided sample を作成できる
    • guidance parameter cg が classifier gradient をスケーリングする
    • より強い guidance は class の特徴を強めるが、realism は低下する可能性がある
  • Classifier-free guidance

    • classifier-free guidance は、別個の classifier なしで guidance を行う
    • Bayes rule を適用すると、class gradient は条件付き score とデータ全体の score の差に置き換わる
    • denoising diffusion model は学習データの score を学習するため、1つの diffusion model を同じサンプルに対して 2 通りの方法で学習する
    • class label (y) とともに学習する
    • null class label とともに学習する
    • サンプリング時には、望む class label を入れた呼び出しと label なしの呼び出しの両方を行い、その差を guidance vector として使う

画像条件と制御

  • Image-to-imageとSDEdit

    • 基本的なimage-to-imageはモデルの再学習なしで可能
    • 入力画像に、望むconditioning strengthに応じてノイズを加えた後、denoiseする
    • conditioningを強くしたい場合は、加えるノイズを少なくする
    • conditioningを弱くしたい場合は、より多くのノイズを加える
    • この方式がSDEditであり、入力画像と同じ全体的な形状を持つ結果を作る
    • 欠点は、入力画像が何を制御するのかを細かく指定できない点にある
    • 入力スケッチがあると、結果がスケッチのように見えたり、指定した形状に十分従わなかったりするトレードオフが生じる
  • Sketch-Guided DiffusionとControlNet

    • スケッチ画像 (y) で生成を条件付けするには、noisy image (x_t) からスケッチ線 (\hat{y}=F(x_t)) を予測するモデルを学習し、sketch loss gradientで各サンプリング段階をguideできる
    • これはSketch-Guided Diffusionのアイデアである
    • 別の方法は、denoiserがconditioning image (y) を受け取るようにarchitectureを変更し、((x_0,y)) ペアでfine-tuningすることである
    • 単純なfine-tuningでは、overfittingやcatastrophic forgettingの問題が生じる可能性がある
    • ControlNetは、元のモデルのweightを保持するためにコピーを作成し、元のモデルはfreezeしたまま、control-netと1x1 convolutionパラメータを学習する
    • convolutionは最初にzeroで初期化され、denoising stepに対するdeltaを段階的に学習する
    • 実際のControlNetは、denoising model全体レベルではなく、per-blockレベルで適用される
    • human evaluationでは、Sketch-Guided Diffusionのような代替手法より優れた性能を示す
    • LoRAと組み合わせることで、consumer GPU上でもControlNetを効率的に学習できる
  • Inpainting

    • Inpaintingは、画像のmasked partを埋める作業である
    • masked partにだけノイズを加える単純なimage-to-image方式は機能しない
    • (t>0) では、denoising modelがnon-noisy partをどのように処理すべきか分からない
    • 機能する方法は、masked partとunmasked partの両方にノイズを加えて (x_T) として入力することである
    • その後、各sampling step (t) で元画像のunmasked partをコピーし、timestep (t) に合わせてノイズを加えたうえで、これを (x_t) の上に配置してdenoiserの入力として使う
  • Text-to-image

    • Text-to-imageは、text embedding labelを使うconditional generationである
    • OpenAIのDall-Eは、画像とテキストを同じ空間に射影するCLIP encoding modelを学習した
    • multimodal embedding spaceが必須というわけではない
    • Google Imagenは、T5 large language modelでテキストをembeddingとしてエンコードする
    • embeddingが十分に豊かな表現であれば、text-to-imageの条件として使える

学習データと倫理

  • 画像生成モデルのデータセットは拡散モデルに特有の問題ではないが、生成モデルを議論するうえで重要な要素である
  • データセットの例は次のとおり
    • Dall-E 1は2億5千万のtext-image pairで学習し、Dall-E 2は6億5千万pairで学習したが、datasetはclosed sourceである
    • Stable Diffusion 1はLAION-2B-enの20億pairで学習した後、LAION-5Bの1億7千万pairでfine-tuningされた
    • その後、Stable Diffusion 1のcheckpointはLAION-5Bの中から「aesthetics」基準で選ばれたsubsetでfine-tuningされた
    • LAION-400Mは2021年8月に公開され、OpenAIがCLIP学習に使用したプロセスを再現しようとする試みだった
    • LAIONはCommon Crawlから派生しており、alt-textのあるHTML image tagを収集し、CLIPで内容と一致しない項目を除外する
  • 一部のユーザーはLAIONに登場するartist listを作成し、haveibeentrained.com では、ユーザーが自分の画像がLAIONや他のdatasetに含まれているか確認できる
  • AI art backlashの大きな要素は、LAIONのようなdatasetがartistの同意なしにartを収集し、画像生成モデルの学習に使用しているという倫理問題である
    • 画像モデルがartistの生計に直接的な脅威となりうる点も合わせて論じられている
  • より倫理的な方法で競争力のある画像生成モデルを学習しようとする取り組みもある
    • Adobe Firefly は、Adobe Stockのようなlicensed contentと、著作権が切れたpublic domain contentのみで学習されているとされている
    • Fireflyが一部のMidjourney画像で学習されたという最近のscandalがあった
    • Stable Diffusion 3はartistに対してtrainingへの使用をopt-outできるようにし、その結果8千万枚を超える画像が削除された

Data poisoning

  • Nightshadeは、AI art backlashの中で注目を集めた、画像生成モデルに対するdata poisoning攻撃である
  • モデルは数十億枚の画像で学習するが、特定のconceptについては画像が数十枚しかない場合がある点を利用する
  • Nightshadeは、concept-specific basisでデータをpoisonしようとする方式である
  • 論文の著者らは、50枚の改変画像でStable Diffusion XLを攻撃し、promptに「car」が含まれるたびにcowを出力させる事例を示した
  • 改変は、perceptual lossを含むmulti-objective functionを最適化し、人間の目にはできるだけ目立たないよう設計されている
  • 初期の攻撃では、モデルのfeature extractorへのアクセスが必要である
  • 著者らは、4つのモデルのうち1つをベースにした攻撃が他のモデルでどのように機能するかを調査し、その攻撃が元のモデル以外にも一般化すると述べている

高解像度画像生成

  • Cascaded Diffusionは、低解像度で初期生成を行った後、複数のsuper-resolution diffusion modelで画像をupscaleする初期のアプローチである
  • Stable Diffusionはlatent diffusionを使用する
    • auto-encoderのlatent spaceでdiffusionにより画像を生成する
    • その後、latentをdecodeして高解像度画像を得る
  • latent diffusionの著者らは、auto-encoderとdiffusion modelを一緒に学習するよりも、まずauto-encoderに画像データを圧縮するよう学習させ、別途encoded latentに対してdiffusion modelを学習する方式が最もうまく機能すると見ている
  • Cascaded Diffusionとlatent diffusionは、backbone diffusion modelに複数の別モデルを組み合わせて生成画像をscale upする
  • single-model resolutionの進歩は、multi-scale lossやtransformerのようなbackbone architectureの利用など、さまざまなtraining trickに基づいている

画像以外での拡散モデルの応用

  • Audio, video, 3D

    • Riffusionは、Stable Diffusionをスペクトログラム画像出力向けにfine-tuningし、12秒の楽曲を生成した初期の音楽生成モデルである
    • Sonautoは、diffusion transformerに基づく、より最近の制御可能なモデルであり、coherent lyricsを持つ1分35秒の楽曲を生成できる
    • OpenAI SoraGoogle Veoは、テキストプロンプトから1分の1080p動画クリップを生成できるdiffusion transformer動画生成モデルである
    • Soraは動画を時空間パッチに分解し、パッチをdenoiseするよう学習する
    • Soraのtechnical reportの中核となるinsightは、diffusion transformerが動画生成においてscaleし、性能がcomputeとともにscaleするという点である
    • OpenAIは、ここでいうcomputeがdataset size、model size、training timeのどれを指すのかを明確にしていない
    • 両モデルは、masked editing、完全なループ動画生成、静止画像のアニメーション化、動画を時間方向の前後に拡張する作業などをサポートする
    • 過去の動画diffusion研究としてはImagen Videoがあり、VideoPoetのようなautoregressive modelも代替手段である
    • 2D diffusion modelは、correspondenceのような3D featureを暗黙的に学習する
    • DreamFusionは、text-to-image diffusion modelをpriorとして用い、gradient descentベースの3D reconstruction algorithmをguideする
    • Stable Video 3Dは、video diffusionによってmulti-view consistencyを改善する
    • これらのモデルは依然として、photogrammetry、3D gaussian splatting、neural radiance fieldのような3D reconstruction algorithmに依存している
    • 可能な理由の1つは、3D dataが比較的希少である点である
  • Life sciences

    • 拡散モデルはmedicineとbiologyでさまざまな応用先を見つけている
    • partial CTとMRI scanは、患者のradiation exposureを大幅に減らしcomfortを高めるが、partial dataからfull scanをreconstructionする必要があるため難しい
    • 拡散モデルはmedical image reconstructionにおいて、supervised methodより優れた性能とgeneralizationを提供し、state-of-the-artを前進させている
    • DeepMindのAlphaFold 3はdiffusion-based architectureを使用しており、以前のバージョンやspecialized toolより大幅な改善を示している
    • 入力されたmolecule listが与えられると、AlphaFold 3はatom cloudから始めて反復的にrefinementし、joint 3D structureを生成する
    • これにより、molecule同士がどのように結合するかを示す
    • そのほかのcomputational biologyでの応用としては、single-cell data analysis、drug and small molecule design、protein-ligand interactionがある
  • Robotics

    • 現実世界と相互作用するrobotは、非常に幅広いphysical behaviorを実行しなければならない
    • 従来の方法では、ドアを開ける、靴ひもを結ぶといった作業に対して、数多くのedge caseやrecovery方法を明示的にプログラミングする
    • factoryのようなcontrolled settingでは機能するが、scaleしない
    • Policy learning from demonstrationは、human demonstrationによってrobotにtask遂行を教える、より拡張性の高いアプローチである
    • 通常は、人がteleoperationでrobot motorを制御してdemonstrationを提供する
    • demonstrationは数十件から数百件必要になることがある
    • その後、robotはsensor observationと、場合によってはnatural language promptを条件としてactionを生成する方法を学習する
    • 拡散モデルはpolicy generation modelにおいてstate-of-the-artであり、従来技術に比べてsubstantial improvementを示している
    • multimodal action distributionを自然に扱える
    • high-dimensional action spaceに適している
    • training stabilityが印象的である

1件のコメント

 
GN⁺ 2024-05-27
Hacker Newsの意見
  • 拡散がスコアマッチングから出てきたものだと思っていたが、今日になって拡散のほうがスコアマッチング理論より先だったと知った
    OpenAIが2億5千万枚の画像で学習したときでさえ、なぜ基底分布をモデリングするのかを説明する優れた理論がなかったわけで、かなり大胆な選択だった
    • もともとDickstein 2015の論文 [1] は、拡散を分布生成の対数尤度の下限を最大化する方法として定式化していたので、理論がまったくなかったわけではない
      ただし突破口はHo [2] とNichol [3] の実験結果だったと理解している。拡散が高品質なサンプルを作れるだけでなく、場合によってはGANより優れていることを示したからだ

      [1] https://arxiv.org/abs/1503.03585
      [2] https://arxiv.org/abs/2006.11239
      [3] https://arxiv.org/abs/2105.05233

  • Diffusion Transformers向けのApacheまたはMITライセンスのPythonライブラリでは、どれが一番良いだろうか?
  • 分類器なしガイダンスで、「すべての学習サンプル x0x0 について、一度はクラスラベル yy と、もう一度は null クラスラベルと組み合わせて単一の拡散モデルを学習する」というやり方は、同じ節の最初の段落で問題だと言っていたこととまったく同じではないのか?
    「特定の yy に対して p(x∣y)p(x∣y) サンプルを十分に見ていなければ、ラベルのせいで非現実的だったり多様性の乏しいサンプルが出る可能性がある。だから生成時にモデルがラベルにどれだけ『従うか』を調整したい」という話と同じ問題に見える
    • まったく同じではない。サンプリングが条件付き分布からそのまま引くのではなく、無条件サンプルを条件付きサンプルの方向へ調整可能な量だけ押し出す形で定式化されているため、現実性とクラスの強さのあいだのトレードオフを制御できる
      ただしガイダンス強度を強くしすぎると現実性が落ちる似た問題が起きる、という点はその通り
  • 「2022年に絵を学ぶのに時間を費やしたのに、Stable DiffusionのようなAIアートモデルの台頭に不意を突かれた。突然、コンピュータが自分の望みうるよりはるかに上手い画家になった」
    それでも著者には描き続けてほしい。AIが創作作業を侵食すればするほど、むしろ全部壊してしまいたくなる
    • 逆に、Stable DiffusionやMidjourneyのようなものに触れてから、絵を描いたりデジタルアートを試したりする動機がむしろ強くなった
      こうしたツールの出力を持ってきて、自分で再現したり模写したりしてみようと思う
  • この記事のおかげで、拡散モデルがどうやって、なぜ動くのかをずっとよく理解できた。ランダム性は不思議なくらい強力だ
    そろそろ、ほどよく不適切な言語でひとつ自分でコーディングしてみる頃合いだ。コメントだけ流し見する人向けに要約することもあまりない。この記事自体がStable Diffusionの要約版だ
  • 学習ループが間違っているのではないか? xts の式に x0s と eps の両方が使われていないので、ただランダムノイズ予測を学習しているように見える
    • どの式のことを言っているのかは分からないが、自分の理解ではネットワークが正解画像を直接「見る」ことはない。その代わり、損失関数を通じて情報を間接的に推論するよう学習しなければならない
      損失関数にはノイズに関する情報が入っており、ネットワークはノイズの混ざった画像を正確に見るので、実際のサンプル画像について学ぶのと等価だ。出力と実画像の差を測る損失関数を設計することもでき、その情報量は同等だが、ガウスノイズの性質のおかげで勾配推定にはノイズ予測のほうがはるかに有利だと知られている。要するに、本物の画像情報はループ内にあるが、ある種のノイズというレンズを通してしか入ってこないということだ

    • その通り。すぐ前の式と同じであるべきだ

      xts = alpha_bar[t].sqrt() * x0s + (1.-alpha_bar[t]).sqrt() * eps

      しかもコードも一貫していない。サンプリングコードでは時間埋め込みを使っているのに、学習コードでは使っていない

  • レイアウトが本当に読みやすい。どうして自分は読みにくいレイアウトを作ることにあんなに時間を無駄にしてしまうのだろう?
    ただ、Reader Viewを有効にしたときは少し惜しかった。「このページは意味論的に完璧だ!!」と証明したがっているようだったが、その環境ではナビゲーション一覧の行間が1未満で、かなり潰れてしまっていた。それでも許せる ; )