論文メモ:EACL2021, How Good (really) are Grammatical Error Correction Systems?

EACL2021,How Good (really) are Grammatical Error Correction Systems? の論文を紹介してみます.

@inproceedings{rozovskaya-roth-2021-good,
    title = "How Good (really) are Grammatical Error Correction Systems?",
    author = "Rozovskaya, Alla  and
      Roth, Dan",
    booktitle = "Proceedings of the 16th Conference of the European Chapter of the Association for Computational Linguistics: Main Volume",
    month = apr,
    year = "2021",
    address = "Online",
    publisher = "Association for Computational Linguistics",
    url = "https://www.aclweb.org/anthology/2021.eacl-main.231",
    pages = "2686--2698",
}

ACL Anthology: aclanthology.org

この論文は,Copyright © 1963–2021 ACLCreative Commons Attribution 4.0 International Licenseの元で公開されています.

概要

文法誤り訂正(GEC, Grammatical Error Correction)の参照あり評価(事前に用意した正解文を使って行う評価)では,用意された正解文が正解の集合を網羅できない問題がある.そこで,システム出力文をできるだけ変えないように人手で修正を加えて,それを正解文とみなして評価した.英語とロシア語のデータセットで評価した結果,GECシステムは従来報告されているよりも高品質な訂正が行えることを示唆した.また,10-bestの出力文の分析からは,下位のランクほど多様な訂正が行えていることを示唆した.

導入

現状の参照あり評価は,GECシステムの性能を過小評価していると主張しています.GECは入力文の誤りを発見して自動で訂正するタスクですが,訂正の候補は山ほどあります.現在使われているM2 ScorerやGLEU,ERRANTのような評価指標はいずれも参照あり評価で,事前に用意された正解文をもとに評価しています.正解文は人手でアノテーションしており,データセットによっては一つの入力文に対して正解文が複数付与されることがありますが,前述したように正解の候補は大量にあるため全てを網羅できません.よって,現状の参照あり評価は,GECシステムが持つ本当の性能を評価できていないとしています.また,こうなる主な原因は,正解文がシステム出力とは独立に作成されているから,とも主張しています.

手法

前述した問題は,システム出力から正解文を作ることで解決します.こうして作られた正解文もまた入力文に対応する正解の集合に属するため,本来は評価されるべき部分が評価されると言えます.論文中では,このように作成した正解文をClosest Golds (CGs)と呼んでいます.これと対応して,元からアノテーションされている正解文はReference Gold (RG) と呼んでいます.また,システム出力の10-bastに対して検証します(本当は1,2,5,10番目ですが).

CGsは,入力文とシステム出力文を見ながら,入力文の意味を損なわず,かつ,システム出力文との編集距離ができるだけ小さくなるように作ります.形式的には,入力文をS,10-bestの出力文を順にH_1, H_2\dots H_{10}とすると,(S,H_i)の組みを見ながら,対応するCG_iを作ることになります.結局,全体的としては,S,H_i,RG,CG_iのセットが得られる感じです.

実験

実験は英語のデータセットとロシア語のデータセットで行っています.英語データセットはCoNLL-2014とBEA test,ロシア語のデータセットはRULEC-GECとlang-8コーパスのロシア語の文です.GECモデルは,英語にはBERTベースのモデル,ロシア語にはTransformerベースのSoTAモデルを使います.

CG_iは,i=1,2,5,10について作ります.つまり,ある入力文に対してGECシステムが1,2,5,10番目だと推定した4つの出力文について,それぞれを人手で編集してCG_iを作ります.入力文は,各データセットから100文ずつランダムに取ります.CGsアノテーションは,英語ではネイティブと非ネイティブの2人,ロシア語は1人のネイティブが行います.3人とも修士号を有しており,過去にアノテーションの経験があるそうです.

結果

モデルの出力はRGに対して最適化されていることを主張しています(Figure2).まず,RGに(赤色)に注目するとH_1からH_{10}にかけてスコアが減少していて.かつ,H_1H_2の差が特に大きく開いています.一方で,CG(青色)に注目すると,RGほどその傾向は表れていません.つまり,モデルの出力文でランクが高いものでも低いものでもやっている訂正は高品質だということです.

f:id:gotutiyan:20210704140051p:plain

また,訂正が行われる数に注目すると,ランクが低い出力文ほど多くの訂正を行っており,かつ,RGに対する性能値とCGに対する性能値の差が大きくなることを示しています(Table2).つまり,ランクが高い出力文ほどRGに近いような訂正をするようにフィットしていることが言えます.(まあtrainもtestも人が正解文をつけているので,trainにフィットさせたモデルが自信を持って出力する文はtestの正解(RG)にもフィットするだろうというのは自然な気はします.)

f:id:gotutiyan:20210704140142p:plain

次に,編集率(Edit Rate)に注目した結果も分析しています(Table3).ここでは主にpost-editと呼ばれる,正解文であるCGとシステム出力文の間の編集距離として定義された値を計算しています.結果として,英語のデータセットではランクによるpost-editに大きな違いはなく,ロシア語のデータセットでは H_5や[tex:H{10}]はわずかにpost-editが大きくなっているが,H_1H_2の差は特にないという結果になっています.ロシア語のデータセットではH_5や[tex:H{10}]でpost-editの値が大きくなりますが,大幅に劣化しているわけではないので,やはり下位のランクの出力文も高品質ですねという結論です.

f:id:gotutiyan:20210704140202p:plain

最後に,英語のデータセットで出力文のランクにおける訂正傾向の違いについて調べた結果は,低いランクの出力文ほどLexical(語彙的)な訂正を行っていることを示唆しています.Table5では上のブロックがスペル誤り/文法誤りでグルーピングした誤りタイプを,下のブロックがLexicalな誤りでグルーピングした誤りタイプを示しています.Table5最下部のパーセンテージを見ると,H_1よりもH_{10}のほうがLexicalな誤りを多く含んでいることがわかります.つまり,実は低いランクの出力文のほうが多様な誤りを訂正できているのではないかということです.

f:id:gotutiyan:20210704140214p:plain

考察など

DiscussionはConclusionみたいになっていますね.3つくらいの主張にまとめています.1つ目は,やはり現状の GECシステムってみんなが思ってるより高性能だよ,ということ.2つ目は,10-bestで1番になっている訂正はRGに寄っているので数値上は最も高品質に見えるが,下位の出力も高品質で,場合によってはトップの出力が負けることもあるということ.3つ目は,10-bestで下位の出力文は,多様な訂正をしているということです.

【Pytorch】 EarlyStoppingを実装する

はじめに

本記事ではpytorchでEarlyStoppingを実装する方法を紹介します.EarlyStoppingはいくつか実装方法がありますので,そのうちの一つを扱います.

おさらい: EarlyStoppingとは

深層学習における教師あり学習では,訓練データを用いて学習を行いますが,やりすぎると過学習してしまいます.過学習を起こしているときは,訓練データのlossは減少する一方で,汎化性能が下がるため開発データのlossは増加することが予想されます.よって,開発データのlossを眺めつつ,増加傾向にあれば学習を打ち切るような工夫をします.このような工夫をEarlyStoppingと呼びます.

使用するリポジトリ

今回は以下のリポジトリの実装を使います.

github.com

上記リポジトリをcloneするか,cloneが面倒であれば pytorchtools.py だけコピーし,手元に準備します.これ以降,from pytorchtools import EarlyStoppingのようにインポートすることになります.pytorchtools はpipにも存在しますが中身が異なるため,必ず上記リポジトリpytorchtools.py を手元に置き,インポートする形にしてください.

この実装では,開発データの最小lossに注目し,最小lossが更新されているかということを打ち切る基準にします.最小lossが順調に更新されていれば学習を続けますし,一定のエポック数が経過しても更新できなければ打ち切ります.

実装

最小限構成

まずは,最小限の構成として以下のようなコードを書いてみます.

from torch import nn
from pytorchtools import EarlyStopping

# 適当なモデル
class Model(nn.Module):
    def __init__(self):
        super(Model, self).__init__()
        self.Linear = nn.Linear(1, 1)
    
    def forward(self, x):
        pass

# インスタンス作成
model = Model()
early_stopping = EarlyStopping(patience=3)
# loss(だと思っている値)のループ
for val_loss in [5, 4, 3, 2, 1, 2, 3, 4, 5, 6]:
    print('val_loss:', val_loss)
    early_stopping(val_loss, model)
    if early_stopping.early_stop:
        print("Early Stopping")
        break # 打ち切り

まずはモデルのインスタンスと,EarlyStoppingインスタンスを作ります.今回は3回連続で最小lossを更新できなかった場合に打ち切ることにするので,patience=3とします.モデルは何らかのパラメータを持つ必要があるので,適当にLinear()を持たせています.

続くfor文では,開発データのlossのつもりで値をループしています.序盤は5,4,3,2,1とlossが減少しますが,途中で過学習が起こって2,3,4,5とlossが増加する状況を想定します.lossが得られるたびに,early_stopping(val_loss, model)として,lossとモデルのインスタンスを渡します.

EarlyStopping はメンバ変数に .early_stop を持っており,打ち切るべきだと判断すれば True になります.if文でこれを確認し,True であればbreakしてエポックのループを打ち切ります.

実行すると以下のようになります.

val_loss: 5
val_loss: 4
val_loss: 3
val_loss: 2
val_loss: 1
val_loss: 2
EarlyStopping counter: 1 out of 3
val_loss: 3
EarlyStopping counter: 2 out of 3
val_loss: 4
EarlyStopping counter: 3 out of 3
Early Stopping

3回連続で最小lossを更新できなかったので,打ち切られたことが確認できます.

これと同時に,最小lossを達成するようなモデルがcheckpoint.ptというファイル名で保存されています.ファイル名は後述するオプションで変更可能です.

実践的な例

実践的な例に関しては,同リポジトリMNIST_Early_Stopping_example.ipynbが非常に参考になります.このノートブックでは,ざっくり以下のような形でEarlyStoppingが使われています.

early_stopping = EarlyStopping(patience=20)
for エポック数:
    for 訓練データのミニバッチ:
        モデルの訓練
    
    for 開発データのミニバッチ:
        開発データのミニバッチlossを計算,リストに保存
    early_stopping(開発データのミニバッチlossの平均値, モデル)
  if early_stopping.early.stop:
    break # 打ち切り

訓練データのミニバッチを一周するたびに開発データのlossを計算します.開発データのlossはミニバッチ単位で複数得られるため,それらの平均値で打ち切るかどうかを判断します.

オプション

early_stopping = EarlyStopping()インスタンスを作る際には,オプションで以下のような設定ができます.

patience= (デフォルト: 7)

開発データのlossが何回連続で最小lossを更新できなければ打ち切るか指定します.例えば,patience=3 であれば,最小lossを3回連続更新できなければ打ち切ります.

verbose= (デフォルト: False)

普通,特別な出力があるのは開発データの最小lossを更新できなかったとき(悪くなったとき)の

EarlyStopping counter: 1 out of 3

みたいな表示だけですが,verbose=Trueのもとでは,最小lossを更新したとき(良くなったとき)にも以下のようなメッセージを出力します.

Validation loss decreased (5.000000 --> 4.000000).  Saving model ...
delta= (デフォルト: 0)

lossが前回からいくつ減少すれば良くなったと判断するか設定します.デフォルトは 0 なので,最小lossから少しでも値が減少していれば良くなったと判断します.一方で,仮に delta=0.1 に設定した場合,最小lossから 0.1以上減らないと,最小lossを更新したとみなされません. deltaを大きくすればするほど,打ち切られやすいと解釈できます.

path= (デフォルト: 'checkpoint.pt')

最小lossが更新された際にモデルが保存されるパスを指定します.

おわりに

今回はPytorchでEarly Stoppingを実装する方法を紹介しました.今回紹介したライブラリは一例に過ぎません.他にもpytorchのigniteというライブラリ群にEarly Stoppingの実装があったりします.お好みのものを探して使ってみてください.

【python】gensimモジュールで分散表現を獲得・保存・読み込む方法を丁寧に

はじめに

本記事では,gensimモジュールを用いてWord2Vecで分散表現を獲得・保存・読み込む方法を紹介します.

公式リファレンス:

radimrehurek.com

目次

分散表現の学習

ここでは生データから分散表現を学習する方法を説明します.具体的には,gensim.models.word2vec.Word2Vec()の関数を用います.入力のデータ構造は単語リストのリストです.

from gensim.models import word2vec

sample_sents = [['this', 'is', 'a', 'first', 'sentence', '.'],
                ['this', 'is', 'a', 'second', 'sentence', '.']]
model = word2vec.Word2Vec(sentences=sample_sents, size=100, window=5, min_count=1)

実行すると,modelに学習結果が格納されます.これは<class 'gensim.models.word2vec.Word2Vec'>というオブジェクトです.

各種オプション

word2vec.Word2Vec()でよく使われるオプションを紹介します.

公式リファレンス:https://radimrehurek.com/gensim/models/word2vec.html#gensim.models.word2vec.Word2Vec

オプション 説明 デフォルト
sentences= 元となるコーパス.単語リストのリスト.
courpus_file= コーパスをファイル読み込みする場合に指定.1行1文の形式で,単語は空白区切りで認識される.
size= 分散表現の次元.リファレンスではvector_sizeと書いてあるように見えるが,sizeでないと動かない. 100
windows= 学習時に利用される文脈の広さ. 5
min_count= 分散表現を獲得する単語の最小頻度.1なら全ての単語について獲得される. 5
workers= 学習時の使用スレッド数. 3
sg= 学習アルゴリズムの選択.1ならskip-gram,0ならCBOW. 0

学習済み分散表現の機能

<class 'gensim.models.word2vec.Word2Vec'>の機能を簡単に説明します.ここでは,以下のコードで獲得した分散表現を用いた例を示します.(本来はsizeを100~300程度にすべきですし,文をもっと増やすべきです.)

from gensim.models import word2vec

sample_sents = [['this', 'is', 'a', 'first', 'sentence', '.'],
                ['this', 'is', 'a', 'second', 'sentence', '.']]
model = word2vec.Word2Vec(sentences=sample_sents, size=3, window=5, min_count=1)
  • ある単語の分散表現を得る.
    .wvWord2VecKeyedVectorsというオブジェクトで,単語をキー,分散表現を値に持つ辞書のように扱えます.
print(model.wv['this'])
# [ 0.12677142 -0.07538117 -0.13080813]
  • 2つの単語の類似度を得る.
print(model.similarity('first', 'second'))
# -0.7543343
  • ある単語と類似している単語を上位 topn件得る.返り値は(単語, 類似度)のリスト.
n = 5
print(model.most_similar('this', topn=n))
[('is', 0.8868916034698486),
 ('second', 0.8849490880966187),
 ('sentence', 0.6720788478851318),
 ('first', 0.5845127105712891),
 ('.', 0.3697856068611145)]
  • 単語ベクトルの足し引き.
    王 - 男 + 女 = 女王 みたいなやつ.positive=に正の項の単語を,negative=に負の項の単語を指定する.topn=で上位 topn件を得る.
print(model.most_similar(positive=['this', 'first'], negative=['second'], topn=1))
# [('is', 0.15704883635044098)]
# this - second + firstということ

# 王の例だと
# most_similar(positive=['king', 'woman'], negative=['man']) のように書ける.

分散表現の保存

学習した分散表現は,.wv.save_word2vec_format(保存ファイルパス)で保存できます.

from gensim.models import word2vec
from gensim.models import KeyedVectors

sample_sents = [['this', 'is', 'a', 'first', 'sentence', '.'],
                ['this', 'is', 'a', 'second', 'sentence', '.']]
model = word2vec.Word2Vec(sample_sents, size=3, window=5, min_count=1)
model.wv.save_word2vec_format('sample_word2vec.txt')

保存結果は以下のようになります.1行目には単語数と分散表現の次元が,2行目以降は分散表現が並んでおり,1行1単語に相当します.

7 3
this 0.12709168 -0.11746123 -0.1590661
is -0.10325706 0.14546975 -0.10878078
a 0.0123018725 0.104428194 -0.069693
sentence 0.16237356 -0.07644377 0.16515312
. 0.09359499 0.12543988 -0.01799449
first -0.019889886 -0.077862106 0.13868278
second 0.060134348 0.029044067 0.03352099

一般には,容量が削減できることから,binary=Trueとしてバイナリファイルで保存・公開されることが多いと思います.

model.wv.save_word2vec_format('sample_word2vec.bin', binary=True)

分散表現の読み込み

.wv.save_word2vec_format()で保存された分散表現は,KeyedVectors.load_word2vec_format(ファイルパス)で読み込めます.

from gensim.models import KeyedVectors

model = KeyedVectors.load_word2vec_format('sample_word2vec.txt')
print(model.wv['this']) 
# [ 0.12677142 -0.07538117 -0.13080813]

バイナリファイルを読み込む場合は,保存のときと同様binary=Trueを指定します.

from gensim.models import KeyedVectors

model = KeyedVectors.load_word2vec_format('sample_word2vec.bin', binary=True)

例えば,学習済みの分散表現として代表的なGoogleNews-vectors-negative300はバイナリファイルで保存されているので,binary=Trueとして読み込みます.

おわりに

今回はgensimモジュールを用いてWord2Vecで分散表現を獲得・保存・読み込む方法を紹介しました.

【Processing】シグモイド関数でイージングを実装する

はじめに

本記事では,Processingによるアート制作において,シグモイド関数を使ってイージングを実装する方法を紹介します.非常にお手軽にイージングを実装できるので,ぜひ使ってみてください.

シグモイド関数とは

シグモイド関数

 f(x) = \frac{1}{1+e^{-x}}

で表される関数で,グラフに書くと以下のようになります. eネイピア数で,2.718...です.

f:id:gotutiyan:20201231111841p:plain
シグモイド関数の概形

desmosでの描画結果:https://www.desmos.com/calculator/64rtnb5z5i

定義域は無限で,値域が (0, 1)であるような関数です.ニューラルネットの活性化関数としてよく登場しますが,グラフの形からイージングに使えそうなことが分かります.

イージングのために使うことを考えると,-5でほぼ0(厳密には, e=2.718とすると 0.0066962987),5でほぼ1(厳密には, e=2.718とすると 0.9933037)となることは知っておくと良いと思います.

最も簡単な適用例

ここでは,円を左から右に移動するような動きに,シグモイド関数を使ってイージングをかけます.ひとまず,テンプレ+シグモイド関数がこちら.ネイピア数は2.718としています.

void setup(){
  size(500,500);
}

void draw(){
  background(0);
}

float sigmoid(float x){
  return 1/(1+pow(2.718, -x));
}

ここから,円を移動させることを考えます.今回は,60フレームかけて左端から右端へと移動させることにしましょう.ひとまず,コードは以下のとおりです.

void setup(){
  size(500,500);
}

void draw(){
  background(0);
  float sig_x = map(frameCount%60, 0, 59, -5, 5);
  float cir_x = map(sigmoid(sig_x), 0, 1, 0, width);
  circle(cir_x, height/2, 20);
}

float sigmoid(float x){
  return 1/(1+pow(2.718, -x));
}
  • sig_xは,シグモイド関数への入力です.フレーム数を60で割った余りは0から59なので,それをmap()を使って-5から5への範囲に変換します.なぜ-5から5にしたかというと,前述したとおり,シグモイド関数は-5でほぼ0,5でほぼ1をとる関数だからです.
  • cir_xは,円のx座標です.シグモイド関数は0から1の値を返すので,それをmap()を使って0からwidthに変換します.

実行例は以下のようになります.

f:id:gotutiyan:20201231112115g:plain

始点と終点の近傍では遅く,その中間では速くなるような動きが実現できています.

イージングの調整

上記の例では,シグモイド関数に-5から5への範囲を入力しましたが,この範囲を調整することで,機敏のあるイージングができるようになります.

以下の例では,,シグモイド関数の入力として-5 ~ 5, -10 ~ 10, -20 ~ 20, -50 ~ 50の4種類を試しています.

void setup(){
  size(500,400);
  textSize(20);
}

void draw(){
  background(0);
  int n[] = {5, 10, 20, 50}; // 範囲の候補
  for(int i=0;i<4;i++){
    text("n="+n[i], 10, 50+100*i);
    float sig_x = map(frameCount%60, 0, 59, -n[i], n[i]);
    float cir_x = map(sigmoid(sig_x), 0, 1, 100, width-20);
    circle(cir_x, 50+100*i, 20);
  }
}

float sigmoid(float x){
  return 1/(1+pow(2.718, -x));
}

実行結果は以下のようになります.入力の範囲を広くすればするほど,待ち時間が長く,動き始めれば一瞬で移動するような動きになります.

f:id:gotutiyan:20201231113700g:plain

その他の適用例

イージングは様々な場面に適用できます.これまでに紹介した円の例では座標の動きにイージングをかけていますが,他の動きに適用する例を示します.

回転にイージング

シグモイド関数の返り値(0, 1)(0, \pi /2)に変換して角度に使用してみます.

void setup(){
  size(500,200);
  rectMode(CENTER);
  textSize(20);
}

void draw(){
  background(0);
  int n[] = {5, 10, 20, 50};
  for(int i=0;i<4;i++){
    text("n="+n[i], 45+110*i, 130);
    float sig_x = map(frameCount%60, 0, 59, -n[i], n[i]);
    float rad = map(sigmoid(sig_x), 0, 1, 0,  PI/2);
    translate(50+120*i, 50);
    rotate(rad);
    rect(0, 0, 50, 50);
    resetMatrix();
  }
}

float sigmoid(float x){
  return 1/(1+pow(2.718, -x));
}

f:id:gotutiyan:20201231115618g:plain

図形の大きさにイージング

シグモイド関数の返り値(0,1)(0, 50)に変換し,正方形の一辺の長さに使用します.

void setup(){
  size(500,200);
  rectMode(CENTER);
  noFill();
  stroke(255);
  strokeWeight(5);
  textSize(20);
}

void draw(){
  background(0);
  int n[] = {5, 10, 20, 50};
  for(int i=0;i<4;i++){
    text("n="+n[i], 30+110*i, 130);
    float sig_x = map(frameCount%60, 0, 59, -n[i], n[i]);
    float leng = map(sigmoid(sig_x), 0, 1, 0, 50);
    rect(50+110*i, 50, leng, leng);
  }
}

float sigmoid(float x){
  return 1/(1+pow(2.718, -x));
}

f:id:gotutiyan:20201231161755g:plain

おわりに

本記事では,イージングをシグモイド関数で実装する方法を紹介しました.

2020年振り返り

昨年の自分も書いてましたんで(以下リンク),今年の自分も今年やったこと,成し遂げたことを書こうと思います(僕の好きな食べ物の一つはイカリング).

gotutiyan.hatenablog.com

言語処理学会2020に参加した

言語処理学会2020に参加し,一件のポスター発表を行いました.

訂正難易度を考慮した文法誤り訂正のための性能評価尺度
○五藤巧 (甲南大), 永田亮 (甲南大/理研), 三田雅人, 塙一晃 (理研)

文法誤り訂正というタスクにおける評価尺度を提案した論文です.このタスクで扱われる誤りには様々な難易度のものが混在しているにもかかわらず,従来の評価尺度ではそれらを一律に扱っていることを問題として提起し,難易度を考慮して性能評価を行う方法を提案しています.

あいにくオンラインでの開催となりましたが,オンラインでもたくさんの方に訪問いただいて,質問をしていただきました.学会の場で発表するのはこれが初めての経験となりました.

COLING2020に参加した

言語処理関係のメジャーな国際会議の一つであるCOLING2020に参加し,一件のポスター発表を行いました.初めての国際会議への参加&発表となりました.

Takumi Gotou, Ryo Nagata, Masato Mita and Kazuaki Hanawa Taking the Correction Difficulty into Account in Grammatical Error Correction Evaluation

内容は言語処理学会2020と同じようなものですが,評価の際に起こる ある問題を解決するための方法を追記し,考察を深めたような形になります.発表は当然英語で,スピーキングとリスニングが問われるのでやはり大変でした.一方で,特に日本人が興味を持つテーマだったこともあり,日本語でやり取りすることも多かったので助かりました.(助かりました,じゃないんだぞ,英語力を鍛えろ)

どのような準備をしたかについて少し書きます.基本的に,あらゆる発表機会について原稿は作らない主義ですが,今回ばかりは一応原稿は作りました.試しにグーグル翻訳の音声入力に向かって原稿を読み上げると半分くらい全然違う単語に認識されたので,正しく認識されるように発音の練習をしていました(この練習に意味があるかはよく分からない).ポスターについては,言語処理学会のポスターを英語に翻訳し,少し構造や文言を変えるようなことをしました.

また,今回の研究で非常にお世話になっているERRANTというツールがあるのですが,そのツールの作者(でありERRANTの論文の著者)の方とも顔を合わせることができました.これは非常に嬉しかったです.

本研究ではツールの公開もしています.

github.com

IVRC2020で企画がSEED STAGEまで進んだ

IVRCVR技術を活用した様々な試みが集うコンテストです(今年度はコンテストではなくチャレンジだ,という声明が出ていますので,この説明は誤りかもしれません).IVRC2020では,投稿された企画が書類審査→SEED STAGE→LEAP STAGE(決勝)と進んでいきます.

弊学でも企画を投稿しており,見事に書類審査を突破,SEED STAGEへ進みました.僕は実装には一切関わっておらず,原案部分で若干のサーベイをした程度なので本当に関わっていないのですが(その証拠に,原稿ではLast Authorですし(Last Authorなんて言い回しあるのか?)),こうして成果が認められたことは嬉しいです.

Processing関係の活動をした

Processingという言語を使うと,四角や円などの図形をプログラムで描くことができます.これを利用して,アート作品を制作する営みが近年盛んになっています.Twitterでは「#つぶやきProcessing」や「#creativecoding」などのハッシュタグでたくさんの作品が公開されています.個人的にもいくつか作っていて,基本的にGitHubTwitterで公開しています.本記事の執筆時点でStarが13もついているので,それなりに見られている気がします.

github.com

また,PCJ ZINEという有志で作る雑誌のようなものにも寄稿させていただきました.Processingがオアシス的存在であることを熱弁しています.このZINEの発行について,関係者の皆様に感謝申し上げます.

pcdtokyo.booth.pm

それから,Processing Advent Calender 2020の初日として記事を投稿しました.この記事内でも同じような振り返りをしています.

gotutiyan.hatenablog.com

NAISTに合格した

NAISTに合格しました.(以下ポエム)この面接は2020年で一番緊張した.学会よりも.そりゃそうだ,学会でヘマしても人生は変わらないが,面接でヘマると人生変わるのでな.でも僕はなぜか受かるだろうという気持ちで臨んだ.それは今思えば,かつて高校入試で落ちた時と同じ気持ちだった.あの時はなぜ俺が落ちたのか分からなかった.でも今度は受かった.僕にとって初めての第一希望への合格という成果だった.

もっと長いポエムが書けそうなので,また別の機会に自分語りをすることにします.

これに関しては受験期を書いています.

gotutiyan.hatenablog.com

ブログ執筆した

本ブログでは,今年は32本の記事を書きました.この記事を入れると33本です.大した内容は書いていませんが,Processing関係ではゲーム制作講座とか,機械学習・深層学習関係ではツールの紹介をメインで書いたように思います.最近徐々にアクセスも増えている(300アクセス/日くらいですけども)ので,ある程度の需要があるのかなーと思っています.来年も何かしら書いていきます.

おわりに

他にもいろいろあった気がしますが,大きな出来事はこんな感じでした.小さなところでは,ポケモンパンのシールホルダーを買えたこと,小学生の頃乗れた自転車が大学4年で乗れなくなっていたが再びちゃんと乗れるようになったこと,コロナをきっかけに育て始めたサボテンが順調に育っていること,Apple Watchが手に入ったこと,などいろいろありました.

来年も,何かしらの成果を上げられるよう,どこかに貢献できるよう,頑張りたいと思います.また来年,2021年振り返りで書くことがありますように...

良いお年を.

アートを切り替えて不思議に魅せる技法について(Processing Advent Calendar 2020 Day1)

本記事は,Processing Advent Calendar 2020の1日目の記事です.10周年だそうで,おめでとうございます!

目次

2020振り返り・Processingコミュニティについて

せっかくのアドカレの記事なので,簡単な振り返り(という名の過去作の宣伝)と,コミュニティ関係の話を少しだけ書きます.

今年もアート制作という形でProcessingにたくさん触れました.今年は20個ほど作品を作っていて,今までの分と合わせてGitHubで公開しています.GitHubにはそれなりに厳選して載せているので,つぶやきProcessingなどの作品も含めるともっと作ったように思います.

github.com

また,コミュニティとの関わりという点では,PCJ ZINEのVol.0に寄稿させていただきました.Processingがオアシス的存在であることを熱弁しております.このZINEの発行について,関係者の皆様に感謝申し上げます.

pcdtokyo.booth.pm

このように,今年は無理なく作品制作もできたし,少しだけコミュニティにも関わることができたしで,非常に楽しいProcessingライフでした.(まだ12月に入ったばかりで気が早いかもしれませんが,)来年も楽しみたいですね.

はじめに

さて,本記事では,「アートを適切なタイミングで切り替えて不思議な感じにする技法」について書きます.イメージとしては,次のようなものです(両方自作です).

f:id:gotutiyan:20201119175650g:plain

f:id:gotutiyan:20201119175751g:plain

うまくやっているものほど見ていて不思議な気持ちになれるので,個人的に好きなパターンの一つです.本記事では,言語としてProcessingを用いて,実装をメインに紹介します.

雛形

全体の構造はこんな感じです.以下では,2種類のアートを90フレーム単位で切り替えます.

void setup(){}
void draw(){
  if(timeCount%180 < 90) アート1();
  else アート2();
}

timeCountはProcessingのシステム変数で,開始時点からのフレーム数を取得できます.また,アート1とアート2は周期性のあるアートを描画する関数で,両者がどこかで同じ絵柄を持つように作ります.あまり抽象化していませんが,上の例だと90フレームごとに切り替わることになるのです.

簡単な実例で!

市松模様で作るアート

では,簡単な実例で詳細を見てみましょう.以下のような,白黒タイルが回転するものを作ります.白と黒は市松模様のように配置します.

f:id:gotutiyan:20201119182038g:plain

まず,雛形を作ります.

void setup(){
  size(500,500);
}

void draw(){
  if(frameCount%180 < 90)art1();
  else art2();
}

void art1(){
  
}

void art2(){
  
}

この例では,「黒のタイルが回転するアート」と,「白のタイルが回転するアート」が切り替わることになります.この2つをそれぞれart1()art2()に書くわけです.今回は,黒タイル側をart1()に,白タイル側をart2()に書くことにします.

前準備

パラメータの準備だけサクッとしましょう.一行/一列に並べるタイルの数をNで表して,タイル一辺の長さをwidth/Nで計算します.それから,回転させる都合上,rectMode(CENTER)を設定します.

int N=10, len;
void setup(){
  size(500,500);
  rectMode(CENTER);
  len = width/N;
}

「黒タイル側」の実装

黒側は,背景を白にして,黒の四角を描画します.市松模様に並べるためには,四角を横にも縦にも一個飛ばしで並べる必要がありますが,これは変数i,jの2重ループを回すとすれば,i+jの偶奇に注目すると簡単に実装できます.黒は奇数のときに書くことにしましょう.

また,四角を回転させて描画する処理は,

  1. 座標軸の原点を,translate()で四角の中心に持ってくる

  2. 座標軸を,rotate()で回す

  3. 原点に四角をrect()で描画する

  4. 座標軸をresetMatrix()で元に戻す

という4ステップで実装できるため,これを全ての四角について行います.

回す角度については,frameCountラジアンに変換する必要があります.今回はmap()を使って,[0,89]の範囲を[0,PI/2]の範囲に変換します.[0.89]は切り替える周期を90フレームにしていることから来ていて,[0,PI/2]は今回のアートが90度回るたびに切り替わることから来ています.

void art1(){
  background(255);
  fill(0);
  float rad = map(frameCount%90, 0, 89, 0, PI/2.0);
  for(int i=0;i<N+1;i++){
   for(int j=0;j<N+1;j++){
     if((i+j)%2 == 1){
       translate(len*i, len*j);
       rotate(rad);
       rect(0, 0, len, len);
       resetMatrix();
     }
   }
  }
}

これで,黒タイル側は完了です.

「白タイル側」の実装

白側は,背景を黒にして,白の四角を描画します.また,ループ変数のi+jが偶数のときに書くことにします.ロジックは黒タイル側と同じです.

void art2(){
  background(0);
  fill(255);
  float rad = map(frameCount%90, 0, 89, 0, PI/2.0);
  for(int i=0;i<N+1;i++){
   for(int j=0;j<N+1;j++){
     if((i+j)%2 == 0){
       translate(len*i, len*j);
       rotate(rad);
       rect(0, 0, len, len);
       resetMatrix();
     }
   }
  }
}

以上で,全体としては以下のようになり,完成です!いかがでしょうか.正直なところ,両方とも処理が似ているので,本来は一つの関数にして引数で調整するべきですが,今回はあえて冗長に書いています.

int N=10, len;
void setup(){
  size(500,500);
  rectMode(CENTER);
  len = width/N;
}

void draw(){
  background(0);
  if(frameCount%180 < 90)art1();
  else art2();
}

void art1(){
  background(255);
  fill(0);
  float rad = map(frameCount%90, 0, 89, 0, PI/2.0);
  for(int i=0;i<N+1;i++){
   for(int j=0;j<N+1;j++){
     if((i+j)%2 == 1){
       translate(len*i, len*j);
       rotate(rad);
       rect(0, 0, len, len);
       resetMatrix();
     }
   }
  }
}

void art2(){
  background(0);
  fill(255);
  float rad = map(frameCount%90, 0, 89, 0, PI/2.0);
  for(int i=0;i<N+1;i++){
   for(int j=0;j<N+1;j++){
     if((i+j)%2 == 0){
       translate(len*i, len*j);
       rotate(rad);
       rect(0, 0, len, len);
       resetMatrix();
     }
   }
  }
}

拡張:切り替わる速さの調節

今回の例では,90フレームごとに切り替えています.60fpsの環境なら1.5秒ごとに切り替わることになりますが,作品によっては速すぎる場合があります.この時は,切り替えるフレーム数を増やせば良いです.

一つの案としては,定数として切り替わるフレーム数を設定する方法があります.これは(適当に付けた)FRAME_UNITという変数を使って,

int FRAME_UNIT = 90;

// draw()では
if(frameCount%(2*FRAME_UNIT) < FRAME_UNIT)art1();
else art2();

//フレーム数 → 角度への変換は
float rad = map(frameCount%FRAME_UNIT, 0, FRAME_UNIT-1, 0, PI/2.0);

という感じで書けます.こうすると,FRAME_UNITを変えるだけで速さ調整ができて,便利です.(もちろん,mapメソッドのPI/2.0というところも,作品によって変わるかと思います.)

考察:この技法はどのようなアートに適用できるか?

ここではこの技法に関する個人的な考察を書きます.間違っているかもしれません.

この技法は,前面の模様と背景の模様の役割を入れ替えながら,前面の模様に対して何かしらの処理を行う技法です.例えば,市松模様の例では,前面の模様のつもりでタイルを市松模様に並べると,その背景も自然と市松模様になるので,これらを一定の周期で入れ替えることで成立しました.他にも,円を敷き詰めた場合には,その背景はアステロイド曲線で囲まれるような図形を並べた模様になるはずです.このような性質から,前面の模様と背景の模様が共に実装可能でなければいけません.例えば,リサージュ曲線で囲まれる領域を敷き詰めたような模様に対しては,その背景の模様を実装することは難しそうです.

また,色についても制約があると思っていて,切り替える瞬間の模様は2色で構成しないと成立しません.この技法による作品がなぜ不思議なのかというと,今まで図形だと思っていたところがいつの間にか背景になるからです.言い換えると,個々に独立していたはずの領域が連続的になるから,とも言えます.このような性質から,例えば,切り替える瞬間の模様に3色を使ってしまうと,ある1色が前面の役割として動いている間にも残りの2色が独立した領域を作ってしまって,不思議には見えません.ただし,3次元空間では,立方体などのように複数の面を備えたオブジェクトを扱えるので,周期の途中で一時的に他の色を出現させることは可能です.しかしこの場合でも,やはり切り替える瞬間の模様は2色だけにしないと成立しないと思います(後述の参考3のリンクを参照).

元々,僕がこの技法を知ったのは,Twitterでdave(@beesandbombs)さんの作品を見たのがきっかけでした.daveさんの作品ではこの技法がよく使われており,学ぶものが多いです.参考として,以下に3作品ほど,ツイートのリンクを貼っておきます.

  • 参考1:2次元

おわりに

今回は,アートを切り替えて不思議に魅せる技法について紹介しました.もっと良い実装方法,関連する話題などありましたら教えていただけますと幸いです.

ありがとうございました.

ICPC アジア地区予選2016-A Rearranging a Sequence 解説

問題: http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1367

C++で解説しています.

問題の概要

数列(1,2,...,n)があります.その後,i=1,2,\dots ,mの順で,e_iを数列の先頭に出します.最終的な数列を出力してください.

例えばsample1では,
1,2,3,4,5
4,1,2,3,5
2,4,1,3,5
5,2,4,1,3
となって,答えを得ます.

解法

まず,e_iの最後にある値ほど先頭に来ることが分かります.よって,最終的な数列は,e_iを逆順にしたものが先頭に来て,それから残りを単に昇順にしたものが後ろにくっついたものになりそうです.sample1では,4,2,5の順で前に出しているので,最終的な数列はその逆順の5,2,4が先頭に来て,残りの1,3が後ろにくっついたものになります.

でも一つだけ罠があって,同じ値が何度も前に出されることがあります.一見ややこしそうに見えますが,2回目以降に前に出された値は,それ以前の位置は気にする必要はありません.とにかく,最後に先頭に出された値ほど,最終的な数列でも先頭にくるということが重要で,値が何回前に出されたかは気にする必要がありません.

このことは,e_iを逆順に出力するときに,初めて出現した値だけを出力することで実現できます.実装としては,「既に出力したか?」を表すフラグ用配列を作っておくことが考えられます.

また,e_iに出現しなかった残りの値の出力方法については,1,2,...,nの値を,フラグ用配列と照らしながら順番に出力するようなことが考えられます.

#include <iostream>
#icnlude <vector>
using namespace std;
int main(){
    int n,m; cin>>n>>m;
    vector<int> e(m);
    // e_iの入力
    rep(i,0,m){
        cin>>e[i];
    }
    // 既に出力したか?を管理するフラグ用配列
    vector<int> outed(n, 0);
    // e_iは後ろから見て
    for(int i=e.size()-1;i>=0;i--){
        // フラグが0なら出力
        if(outed[e[i]-1] == 0){
            cout<<e[i]<<endl;
            // フラグを1に
            outed[e[i]-1] = 1;
        }
    }
    // 残りを出力.1~nまで回して,フラグが0なら出力
    for(int i=1;i<=n;i++){
        if(outed[i-1] == 0){
            cout<<i<<endl;
            outed[i-1] = 1;
        }
    }
}