Saturday, August 6, 2022

Train a sinusoidal one

 $ fairseq-hydra-train -m --config-dir fairseq/examples/roberta/config/pretraining --config-name sinusoidal task.data=/home/jason-chou/Downloads/$DATA_DIR

Thursday, August 4, 2022

RoBERTa uses learned position embeddings!

 args.encoder_learned_pos = safe_getattr(args, "encoder_learned_pos", True)

So...I need to find a variation that uses sinusoidal position embeddings to test long.

Transformer Language Models without Positional Encodings Still Learn Positional Information is the only preprint/paper mentioning MLM ALiBi so far?

Wednesday, August 3, 2022

PyTorch checkpoint state dict

Sadly RobertaModel doesn't give you the full picture:

>>> from fairseq.models.roberta import RobertaModel

>>> roberta = RobertaModel.from_pretrained('./roberta.base/')

One has to load from the checkpoint themself:

>>> path = './roberta.base/model.pt'

>>> with open(path, 'rb') as f:

>>>   state = torch.load(f, map_location=torch.device("cpu"))

>>> state['args']

Namespace(no_progress_bar=False, log_interval=25, log_format='json', tbmf_wrapper=False, seed=1, cpu=False, fp16=True, memory_efficient_fp16=True, fp16_init_scale=4, fp16_scale_window=128, fp16_scale_tolerance=0.0, min_loss_scale=0.0001, threshold_loss_scale=1.0, user_dir=None, criterion='masked_lm', tokenizer=None, bpe=None, optimizer='adam', lr_scheduler='polynomial_decay', task='masked_lm', num_workers=2, skip_invalid_size_inputs_valid_test=True, max_tokens=999999, max_sentences=16, required_batch_size_multiple=1, dataset_impl='mmap', train_subset='train', valid_subset='valid', validate_interval=1, disable_validation=False, only_validate=False, max_sentences_valid=16, curriculum=0, distributed_world_size=512, distributed_rank=0, distributed_backend='nccl', distributed_port=19812, device_id=0, distributed_no_spawn=False, ddp_backend='c10d', bucket_cap_mb=200, fix_batches_to_gpus=False, find_unused_parameters=True, arch='roberta_base', max_epoch=0, max_update=500000, clip_norm=0.0, sentence_avg=False, update_freq=[1], lr=[0.0006], min_lr=-1, use_bmuf=False, global_sync_iter=10, restore_file='checkpoint_last.pt', reset_dataloader=True, reset_lr_scheduler=False, reset_meters=False, reset_optimizer=False, optimizer_overrides='{}', save_interval=1, save_interval_updates=2000, keep_interval_updates=-1, keep_last_epochs=-1, no_save=False, no_epoch_checkpoints=True, no_last_checkpoints=False, no_save_optimizer_state=False, best_checkpoint_metric='loss', maximize_best_checkpoint_metric=False, adam_betas='(0.9, 0.98)', adam_eps=1e-06, weight_decay=0.01, force_anneal=None, warmup_updates=24000, end_learning_rate=0.0, power=1.0, total_num_update=500000, sample_break_mode='complete', tokens_per_sample=512, mask_prob=0.15, leave_unmasked_prob=0.1, random_token_prob=0.1, activation_fn='gelu', dropout=0.1, attention_dropout=0.1, encoder_embed_dim=768, encoder_layers=12, encoder_attention_heads=12, encoder_ffn_embed_dim=3072, pooler_activation_fn='tanh', max_positions=512, activation_dropout=0.0)

Revisit roberta-large embeddings

def topk_similar_tokens(roberta, index, k, normalize=False, beta=100.):

  embed_tokens = roberta.get_parameter('model.encoder.sentence_encoder.embed_tokens.weight')

  if normalize:

    embed_tokens = embed_tokens / embed_tokens.norm(dim=1, keepdim=True)

  prob = (beta * embed_tokens[index] @ embed_tokens.T).softmax(dim=-1)

  values, indices = prob.topk(k)

  # Print the result

  print("\nTop predictions:\n")

  for value, index in zip(values, indices):

    print(f"{roberta.decode(index.unsqueeze(0)) if index.item() != roberta.task.source_dictionary.pad() else '<pad>'}: {100 * value.item():.2f}%")

>>> topk_similar_tokens(roberta, roberta.task.mask_idx, 10, normalize=True, beta=10.)


Top predictions:


<mask>: 0.54%

: 0.02%

 the: 0.01%

 and: 0.01%

,: 0.01%

 to: 0.01%

.: 0.01%

 that: 0.01%

 in: 0.01%

 GG: 0.01%

>>> topk_similar_tokens(roberta, roberta.task.source_dictionary.bos(), 10, normalize=True, beta=10.)


Top predictions:


: 2.59%

<mask>: 0.02%

: 0.01%

.: 0.01%

 the: 0.01%

,: 0.01%

 a: 0.01%

!: 0.01%

。: 0.01%

?: 0.01%

>>> topk_similar_tokens(roberta, roberta.task.source_dictionary.pad(), 10, normalize=True, beta=10.)


Top predictions:


<pad>: 74.20%

: 0.02%

channelAvailability: 0.02%

PsyNetMessage: 0.01%

 guiIcon: 0.01%

NetMessage: 0.01%

: 0.01%

?????-?????-: 0.01%

 0.01%

EStreamFrame: 0.01%

>>> topk_similar_tokens(roberta, roberta.task.source_dictionary.eos(), 10, normalize=True, beta=10.)


Top predictions:


: 0.79%

.: 0.03%

<mask>: 0.03%

,: 0.02%

 (: 0.02%

 ": 0.02%

 and: 0.02%

 the: 0.02%

 The: 0.02%

-: 0.01%


Nothing shocking here. Other than <pad> it needs to get quite cold in terms of beta for the aligned embeddings to stand out though.

Tuesday, August 2, 2022

validate roberta.base with wikitext-103

$ python3 fairseq_cli/validate.py examples/language_model/data-bin/wikitext-103 --path ~/Downloads/roberta.base/model.pt --task masked_lm --max-tokens 512

2022-08-02 14:16:30 | INFO | fairseq.data.data_utils | loaded 3,760 examples from: examples/language_model/data-bin/wikitext-103/valid

2022-08-02 14:16:30 | INFO | fairseq.tasks.masked_lm | loaded 580 blocks from: examples/language_model/data-bin/wikitext-103/valid

2022-08-02 14:43:30 | INFO | valid | valid on 'valid' subset | loss 2.055 | ppl 4.16 | wps 0 | wpb 246983 | bsz 580


$ python3 fairseq_cli/validate.py examples/language_model/data-bin/wikitext-103 --path ~/Downloads/roberta.base/model.pt --task masked_lm --max-tokens 1024


2022-08-02 15:04:04 | INFO | fairseq.data.data_utils | loaded 3,760 examples from: examples/language_model/data-bin/wikitext-103/valid

2022-08-02 15:04:04 | INFO | fairseq.tasks.masked_lm | loaded 580 blocks from: examples/language_model/data-bin/wikitext-103/valid

2022-08-02 15:31:16 | INFO | valid | valid on 'valid' subset | loss 2.055 | ppl 4.16 | wps 0 | wpb 246983 | bsz 580


$ python3 fairseq_cli/validate.py examples/language_model/data-bin/wikitext-103 --path ~/Downloads/roberta.base/model.pt --task masked_lm --max-tokens 256


2022-08-02 15:33:29 | INFO | fairseq.data.data_utils | loaded 3,760 examples from: examples/language_model/data-bin/wikitext-103/valid

2022-08-02 15:33:29 | INFO | fairseq.tasks.masked_lm | loaded 580 blocks from: examples/language_model/data-bin/wikitext-103/valid

Traceback (most recent call last):

  File "/Users/jason_chou/Documents/GitHub/fairseq/fairseq_cli/validate.py", line 153, in <module>

    cli_main()

  File "/Users/jason_chou/Documents/GitHub/fairseq/fairseq_cli/validate.py", line 147, in cli_main

    distributed_utils.call_main(

  File "/Users/jason_chou/Documents/GitHub/fairseq/fairseq/distributed/utils.py", line 369, in call_main

    main(cfg, **kwargs)

  File "/Users/jason_chou/Documents/GitHub/fairseq/fairseq_cli/validate.py", line 93, in main

    itr = task.get_batch_iterator(

  File "/Users/jason_chou/Documents/GitHub/fairseq/fairseq/tasks/fairseq_task.py", line 295, in get_batch_iterator

    batch_sampler = dataset.batch_by_size(

  File "/Users/jason_chou/Documents/GitHub/fairseq/fairseq/data/base_wrapper_dataset.py", line 61, in batch_by_size

    return self.dataset.batch_by_size(

  File "/Users/jason_chou/Documents/GitHub/fairseq/fairseq/data/fairseq_dataset.py", line 145, in batch_by_size

    return data_utils.batch_by_size(

  File "/Users/jason_chou/Documents/GitHub/fairseq/fairseq/data/data_utils.py", line 340, in batch_by_size

    return batch_by_size_fn(

  File "fairseq/data/data_utils_fast.pyx", line 108, in fairseq.data.data_utils_fast.batch_by_size_fn

    cpdef list batch_by_size_fn(

  File "fairseq/data/data_utils_fast.pyx", line 123, in fairseq.data.data_utils_fast.batch_by_size_fn

    return batch_by_size_vec(indices, num_tokens_vec, max_tokens,

  File "fairseq/data/data_utils_fast.pyx", line 30, in fairseq.data.data_utils_fast.batch_by_size_vec

    assert max_tokens <= 0 or np.max(num_tokens_vec) <= max_tokens, (

AssertionError: Sentences lengths should not exceed max_tokens=256


The first two runs are probably equivalent. I need to figure out how to actually change the context length. Exact same with max_tokens=2048 and 4096 as well:

$ python3 fairseq_cli/validate.py examples/language_model/data-bin/wikitext-103 --path ~/Downloads/roberta.base/model.pt --task masked_lm --max-tokens 2048


2022-08-02 15:48:54 | INFO | fairseq.data.data_utils | loaded 3,760 examples from: examples/language_model/data-bin/wikitext-103/valid

2022-08-02 15:48:55 | INFO | fairseq.tasks.masked_lm | loaded 580 blocks from: examples/language_model/data-bin/wikitext-103/valid

2022-08-02 16:16:02 | INFO | valid | valid on 'valid' subset | loss 2.055 | ppl 4.16 | wps 0 | wpb 246983 | bsz 580  


$ python3 fairseq_cli/validate.py examples/language_model/data-bin/wikitext-103 --path ~/Downloads/roberta.base/model.pt --task masked_lm --max-tokens 4096


2022-08-02 16:18:01 | INFO | fairseq.data.data_utils | loaded 3,760 examples from: examples/language_model/data-bin/wikitext-103/valid

2022-08-02 16:18:01 | INFO | fairseq.tasks.masked_lm | loaded 580 blocks from: examples/language_model/data-bin/wikitext-103/valid

2022-08-02 16:45:08 | INFO | valid | valid on 'valid' subset | loss 2.055 | ppl 4.16 | wps 0 | wpb 246983 | bsz 580  

Monday, August 1, 2022

What tokens are most similar to , , , and in roberta-large?

>>> import torch

>>> roberta = torch.hub.load('pytorch/fairseq', 'roberta.large')

>>> def topk_similar_tokens(roberta, index, k, normalize=False):

...   embed_tokens = roberta.get_parameter('model.encoder.sentence_encoder.embed_tokens.weight')

...   if normalize:

...     embed_tokens = embed_tokens / embed_tokens.norm(dim=1, keepdim=True)

...   _, indices = (embed_tokens[index] @ embed_tokens.T).topk(k)

...   return indices

... 

>>> roberta.decode(topk_similar_tokens(roberta, roberta.task.mask_idx, 10))

' TM JC CSI Zeus Karma CG BG GG MM Harmony'

>>> roberta.decode(topk_similar_tokens(roberta, roberta.task.source_dictionary.bos(), 10))

'.?*.,. *.!.,,.*+.-.'

>>> roberta.decode(topk_similar_tokens(roberta, roberta.task.source_dictionary.pad(), 10))

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

  File "/Users/jason_chou/.cache/torch/hub/pytorch_fairseq_main/fairseq/models/roberta/hub_interface.py", line 75, in decode

    sentences = [

  File "/Users/jason_chou/.cache/torch/hub/pytorch_fairseq_main/fairseq/models/roberta/hub_interface.py", line 76, in <listcomp>

    self.bpe.decode(self.task.source_dictionary.string(s)) for s in sentences

  File "/Users/jason_chou/.cache/torch/hub/pytorch_fairseq_main/fairseq/data/encoders/gpt2_bpe.py", line 41, in decode

    [int(tok) if tok not in {"<unk>", "<mask>"} else tok for tok in x.split()]

  File "/Users/jason_chou/.cache/torch/hub/pytorch_fairseq_main/fairseq/data/encoders/gpt2_bpe.py", line 41, in <listcomp>

    [int(tok) if tok not in {"<unk>", "<mask>"} else tok for tok in x.split()]

ValueError: invalid literal for int() with base 10: '<pad>'

>>> roberta.decode(topk_similar_tokens(roberta, roberta.task.source_dictionary.pad(), 10)[1:])

'channelAvailability\x05EngineDebug<|endoftext|>PsyNetMessage 裏覚醒'

>>> roberta.decode(topk_similar_tokens(roberta, roberta.task.source_dictionary.eos(), 10))

' \u200b,, .... TM ..….. \u200e…… MM'

>>> roberta.decode(topk_similar_tokens(roberta, roberta.task.mask_idx, 10, normalize=True))

'<mask> the and, to. that in GG'

>>> roberta.decode(topk_similar_tokens(roberta, roberta.task.source_dictionary.bos(), 10, normalize=True))

'<mask>. the, a!。?'

>>> roberta.decode(topk_similar_tokens(roberta, roberta.task.source_dictionary.pad(), 10, normalize=True))

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

  File "/Users/jason_chou/.cache/torch/hub/pytorch_fairseq_main/fairseq/models/roberta/hub_interface.py", line 75, in decode

    sentences = [

  File "/Users/jason_chou/.cache/torch/hub/pytorch_fairseq_main/fairseq/models/roberta/hub_interface.py", line 76, in <listcomp>

    self.bpe.decode(self.task.source_dictionary.string(s)) for s in sentences

  File "/Users/jason_chou/.cache/torch/hub/pytorch_fairseq_main/fairseq/data/encoders/gpt2_bpe.py", line 41, in decode

    [int(tok) if tok not in {"<unk>", "<mask>"} else tok for tok in x.split()]

  File "/Users/jason_chou/.cache/torch/hub/pytorch_fairseq_main/fairseq/data/encoders/gpt2_bpe.py", line 41, in <listcomp>

    [int(tok) if tok not in {"<unk>", "<mask>"} else tok for tok in x.split()]

ValueError: invalid literal for int() with base 10: '<pad>'

>>> roberta.decode(topk_similar_tokens(roberta, roberta.task.source_dictionary.pad(), 10, normalize=True)[1:])

'channelAvailabilityPsyNetMessage guiIconNetMessage\x05?????-?????-\x1bEStreamFrame'

>>> roberta.decode(topk_similar_tokens(roberta, roberta.task.source_dictionary.eos(), 10, normalize=True))

'.<mask>, ( " and the The-'


Conclusion: in terms of their embeddings, they are most similar to meaningless tokens.
(Unnormalized inner product is informative here since 
untie_weights_roberta=False)

Friday, July 31, 2020

Summer Pockets SS《Sequela》

Summer Pockets RB グランドエンドに思うところがあって、自己完結の為に書いたようなSS。ネタバレしまくっているから、グランドエンドを見る上に読むのはお勧めです。











夕食後。

「最近観光客が増えて、食堂の客も増えてきた。在庫はまだ余裕があるけど、やっぱり人手も増えておいた方がいいと思う」
「……?ああ、そうね。私はまだ平気だと思うが……」

おかーさんと私2人で片付けた後、おかーさんとおとーさんはお茶を飲みながら食堂の運営とか島の行事とか色々話している。私も付いていけるが、会話に入れるところが少ないから、結局最近入手したゲームをする。おかーさんも、どこか心ここにあらずって感じ。疲れている……というより、多分心配事。もしそうだったら、その対象も多分私だと思う。

正直、おかーさんの実家のお陰で経済面では何の不安もない。食堂の運営も、おかーさんの思い出の所で、自分が続けたいからやっているだけ。ひーじーじがまだ漁師を続けているのも、多分同じだと思う。

その代わり、おかーさんは最近私に対してやや過保護だと思う。自分が私に教えられるのはもちろん、蒼さんに改めて「七影蝶から自分の身を守る術を、羽未ちゃんに出来るだけ教えてください」とお願いして、私を空門家に通わせるのも吝かではなかった。そして、おかーさんと私のことだから、教えられるのは空門家の役目に直結すること以外の全てになってしまう。役に立つと分かって、大変だと思いつつも勉強してきた。でも、どうして?

おとーさんはおかーさんを娶った形で結婚したから、私の名前は「鷹原羽未」になっている。つまり、鳴瀬家の後を継ぐ人はいなくなってしまった。島の年寄り達は大慌てて、ひーじーじも一応分家に連絡を取っているけど、おかーさんの血を引いているから私は何の形で継ぐべきだと主張する人も少なくはない。それでも、今そんな話をするのは早すぎると思う。例え私は鳴瀬家の後を継ぐとしても、今一番勉強しなければならないのも自分を守る術ではないと思う。

……空門家に通うお陰で楽しいことも一杯あるから、良いけどね。

最近入手したゲームというのは、何と島モンファイトのスマートフォンアプリβ版で、私もテスターの一人になっている。藍さんの夢でしかなかった島モンは、今はすっかり地域限定のゲームとして鳥白島の観光事業の一環になっている。私には馴染みすぎたゲームなので、今もかなりの上位ランキングファイター。もうありえない過去の中で夢見た経験なんて、もはや訳がわからないけど、島モンは島モンだから。

島モンのデッキ編成しようとしたが、考え事がオーバーフロー気味でどうしても集中できない。……七影蝶に触ると記憶が混ざって、脳に負担がかかるとしているけど、それじゃないよね……?

「……羽未ちゃん」

不意に、声を掛けられた。

「何〜おかーさん?」
「……七海の事、覚えている?」
「……え?」

簡単な質問なのに、意味が分かるまで、数秒かかった。

そういうことか……
おかーさん、もう気づいていた。

「……もちろん、覚えているよ」

認めたくないが、また嘘をつくのはもっと嫌なので、そうとしか答えられなかった。

すると、おかーさんは相変わらずの優しい顔で、複雑の表情をしている。
嬉しそうに。
心配そうに。
そして、悲しそうに……

「羽依里、ごめんなさい。どうしても、羽未ちゃんと話したい事があって……」
「とても大事な話しなんだろう。俺は聞いちゃダメか?」
「……私はいいけど、羽未ちゃんの意思にもよるから……」
「……分かった」

おとーさんが席を立って、私の頭を撫でながら「お母さんの言う事ちゃんと聞いてね」とだけ言っておいて、自分の部屋に戻った。

おかーさんは言葉を選びながら、何とか言い始めた。

「最近、どこか変……と思う。特に、夏休み最後の日に言っていたのは、どうしても気になっているから。あの日、羽未ちゃんが確かに『言いたかったの。ずっと……2人に……おはようって』と」

私が戻ったのは、今年の夏。
永い永い旅の終わりにまたおかーさんとおとーさんに会えて、思わず泣きそうになって、訳がわからない言葉を言っちゃった……

「あの日を境に、羽未ちゃんは前よりも泣き虫で大人しくなって、よく甘えにきたし。そして何より……」
「うん?」
「あの日のチャーハンは、初めての100点だった。もしかしたらと思って……やっぱり今の羽未ちゃんは、私の小さい頃に会った羽未ちゃん?」

経験を重ねるだけで、人が変わるというのは、本当なのね。

「……」

もう何か月も過ぎても、おかーさんとおとーさんに「おはよう」って言う時、どうしても抱きしめたい衝動を堪えながら、泣きそうになって……

「羽未ちゃん、大丈夫?羽未ちゃんは……この未来に背中を押してくれた恩人で、たった一人の娘。例え入れ替わっても、受け入れるつもり……でも、羽未ちゃんが無理矢理我慢するのは、あまりにも辛いから」

「……あの日までの最高点は、確かに83点だったね」

「え?」

予想外の返事で、おかーさんが目を丸くした。

「私も……よくわかんない。気づいたら、もう夏の日差しで、昼寝をしていた。あの日までの私は……あまりにも違和感がないから、自覚すらできないの。私、もうしかしたら、また無意識に赤ちゃんの頃に戻って……」

「多分、違うと思うよ」
「うん?」
「夏まで、そんなことはなかったし」

何のこと?

「過去に戻れるというのは、未来も視られるということ……って、あっている?」
「……うん」
「今年の夏まで、一度もなかったから。未来を見られるような事」

……何?

「……今朝までの天気予報は、今日一日中ずっと晴れの筈。なのに、羽未ちゃんはお父さんに傘を持たせて、午後は突然、土砂降りの雨が……」
「……っ!」

……あぁ。しまった。
傘を持たずにそんな雨に降られながら帰るのは、あまりにも可哀想だから……
最近は一時間ごとの天気予報まであるし、よく当たるし、遂にうっかりした。
そうか。今日は朝まで、一日中ずっと晴れの予報になっていたのか。

絶対にダメと思いつつ、バレてしまった。

「…もう……おかーさんはおしまい」
「……近づかないで……」

今度もこんな私に怯えて、ここから逃げるだろうか。

「……もう、おかーさんじゃない……」
「……私は…呪われているから……」

今度はおとーさんがおかーさんを見つけて、連れ戻してくれるだろうか。

「……ごめんなさい……」
「……ごめんなさい……」
「……ごめんなさい……」

今度は駄目なら、またおとーさんと2人で島を出て、あそこに戻って……

「……羽未ちゃん、怖くない、怖くない、大丈夫だから……!」
「……えっ?」

いつの間にか、おかーさんは私の隣に、恐怖に震えている私を強く抱き締めている。

「おかーさんは、怖くないの?」
「羽未ちゃんが?どうして?」
「どうしてって……」

ああ、今のおかーさんは同じ力を発現していなくて、いきなり分からないのか。

「……過去に戻れるというのは、未来も視られるということ。でも、同じ力を発現していたおかーさんは、ずっと思い込んでいた……嫌な未来を視てしまったら、その未来へと歩まされるって。そして、私も未来予知ができると分かって、一度私に怯えて、拒絶したから……」

「っ……」

おかーさんはかなりのショックを受けた。
覚えている限り、私を受け入れた上に拒絶したのは、その一回しかなかったから。
あの時の私は、もうぼろぼろだった。だから何も考えずに、正直に言っちゃった。
責任は私にもあるけど。

「……教えてくれればいいのに。そしたら、ちゃっと謝るのに」
「そんな余裕は無かったの。出来るだけ、おかーさんの楽しみを作らなくちゃ」

幼いおかーさんに嫌な記憶を与えてしまったら、すべて無駄になる。
ズルをしなくちゃいけないのは、私だったから。

「……分かった。もうあの記憶を見られないけど、本当に……ごめんなさい。お父さんもお母さんも……羽未ちゃんが隠し事をする必要がないようにするから……もう我慢しなくていい。泣きたい時、思い切り泣いていいの。抱きしめたい時、痛いほど抱きしめてもいい。いったい何年も……無理してきたの?」

あぁ、そんなこと言っちゃ駄目。
覚えていないの、そんなの。

「うう……あぁ……」

でも、せっかく言ってくれたから、そうさせて貰おうか。

「うわわああああああぁぁぁぁっ…………!」

* * *

どれだけ取り乱したのか。
無くはなっていないけど、ちょっとだけ軽くなった。
おとーさんもきっと気づいた。降りてきたかわからないが……
だからどうなの!おかーさんが許したから!

「……一つだけ、約束してくれる?」
「何?」

顔を上げて、まだちょっとぼやけた視界の中でも、おかーさんの目はちょっと赤い。
……痛くないよね?

「もう絶っ対に、過去に戻る力を使っちゃ駄目。お父さんの為にも、お母さんの為にも。今度こそ、羽未ちゃんは名前通り、未来に羽ばたくの。」
「……」

ああ、やっぱりか。
全部ぶっちゃけたから、それは多分おかーさんが一番欲しい約束。

もうしない。
どうなっても。

約束できたら、どれ程おかーさんが喜ぶだろうか。
一度は命を賭けても、出来なかった事だから。
でも……

「……ごめんなさい。嘘をつくのも、約束を破るのも、嫌なの」
「……っ」
「まだ、足りない。まだ全っっ然、足りない。おかーさんとおとーさんに何があったら、私はおかーさんとおとーさんを取り戻す為にどんな力だって使う。おかーさんは違う?」

「……るいよ」
「え?」
「ずるいよ……私に、駄目だと言っておいて……!」

おかーさんは強く唇を噛みながら、やっとの事でそんな言葉をできた。

そう。ズルをしたのは、私なの。
よく考えたら、過去を縋った者、過去を縋るなと言っても説得力ゼロなのね。
でも、これからの事、もう誰も分からない。
未来の記憶を届ける人も、もういない。あんな奇跡、二度と起こらない。
今はただ、おかーさんとおとーさんに一杯一杯してもらいたい……

私はおかーさんの目から逃げるように、また顔をおかーさんのお腹に埋める事にする。

余計に心配させるから、言わないけど……実は今の私、前よりも思い通りに戻れる気がする。

まるで上達してしまったように。

経験を重ねて、人が変わる。もう人の身では、あり得ないほど過去に繰り返してきたから……

「……今は駄目でも、いつかは絶対に羽未ちゃんに満たさせてもらって、約束させる。それは、私の役目。もう私には、それしか出来ないから……」

やっと、おかーさんがため息をついて、許してくれた。

「うん……ありがとう、おかーさん。そして……ごめんなさい……」

それしか、応えられなかった。

もう夜は遅いし、おかーさんに撫でられたまま、うとうとし始めた。
夏じゃないし、いいか。

欲を言えば……子守歌、歌ってくれるかな?
眠りに落ちたら、どんな夢を見るだろう?
せめて今だけ、嫌な未来を視ないように、心から願う。










ちょっとした後書き

サマポケRBの終わり方だって、すべてめでたしめでたしのわけではないと思う。

元々羽未の時空のおとーさんは、一人置いて行かれたまま。
帰って来たあの夏の日までの羽未は、どうなっているの?
羽未自身が言い出すまで、自分の存在は隠し事になる。
PocketのしろははAlkaのしろはではない。あくまで七海としての羽未と出会って、ある程度の記憶と感情が七影蝶を介して共有するだけ。Alkaの「あのシーン」もあるし、受け入れるかどうか……
羽未がまだ力を持っている場合、それは相変わらず諸刃の剣になる。
このまま鳴瀬家が途絶えるが、それは最早ささやかな事。
個人的にはこういう落としどころもあると思いますが、皆さんはどうでしょうか?

Sequela:【名】後遺症