werry-chanの日記.料理とエンジニアリング

料理!コーディング!研究!日常!飯!うんち!睡眠!人間の全て!

FletのCheckBoxに値を保持させてon_changeイベントでvalue参照&複数引数も実装

Flutter baseのpythonクロスプラットフォームGUIライブラリのFLET
まだまだ開発途上なのか工夫しないと複雑な実装は難しいです。

例えばcheckboxに値を持たせても, on_changeイベントからは, どのcheckboxが押されているのか参照不能な状態です。
checkbox以外のオブジェクトについても同様の問題があります。

今回はcheckboxを例に, オブジェクトの値を保持し, イベント発火されたオブジェクトを特定してみましょう。
さらに今回は, イベント発火時に複数引数を持たせる部分まで実装して使い勝手を良くしていきます。

まずは自分の値を参照可能な自作クラスを公式参考に作っていきます。
公式よりUser controls | Flet

import flet as ft

class my_CheckBox(ft.UserControl):
    def __init__(self, label, value:bool = False):
        super().__init__()
        self.label = label
        self.value = value 
    def build(self):
        # イベント関数はbuild直下に書かないと動かない
        def on_change(e, message: str):
            # 自作checkboxはイベント発火後に自前実装で値の変更必要
            self.value = not(self.value)
            print(self.label, 'is changed!!', 'value:', self.value)
            print('message', message, end='\n\n')
            self.update()
        return ft.Checkbox(label=self.label,
                           value=self.value,
                           on_change=(lambda e: on_change(e, message=self.label)))
       # lamda式を使ってイベント発火に複数引数を持たせる

ほぼほぼ完成です。
あとはこいつをmainに載せて実験するだけです。

とりあえずListViewで100個くらいcheckbox作って適当に押してみます。

import flet as ft

class my_CheckBox(ft.UserControl):
    def __init__(self, label, value:bool = False):
        super().__init__()
        self.label = label
        self.value = value 
    def build(self):
        # イベント関数はbuld直下に書かないと動かない
        def on_change(e, message: str):
            # 自作checkboxはイベント発火後に自前実装で値の変更必要
            self.value = not(self.value)
            print(self.label, 'is changed!!', 'value:', self.value)
            print('message', message, end='\n\n')
            self.update()
        return ft.Checkbox(label=self.label,
                           value=self.value,
                           on_change=(lambda e: on_change(e, message=self.label)))
       # lamda式を使ってイベント発火に複数引数を持たせる

def main(page):
    lv = ft.ListView(expand=1, spacing=10, padding=20)

    for i in range(100):
        lv.controls.append(my_CheckBox('my_CheckBox' + str(i)))
    page.add(lv)

ft.app(target=main)
生成された初期画面
my_CheckBox1を押してみる

これできちんと実装できていればterminalに以下のような表示が出てきます。

my_CheckBox1 is changed!! value: True
message my_CheckBox1
次はmy_CheckBox3を押してみる
my_CheckBox1 is changed!! value: True
message my_CheckBox1

my_CheckBox3 is changed!! value: True
message my_CheckBox3

他の番号を押したらきちんと表示が変わりましたね。

次は, checkboxのbool値がきちんと変更されるか試してみます。

my_CheckBox3のチェックを外してみる
my_CheckBox1 is changed!! value: True
message my_CheckBox1

my_CheckBox3 is changed!! value: True
message my_CheckBox3

my_CheckBox3 is changed!! value: False
message my_CheckBox3

ちゃんとチェック外れたことを認識できていますね。

複数引数のmessage部分にも正しい値が渡されていることが見て取れます。


FletはPythonだけでクロスプラットフォームGUI実装できるので便利です。
しかしながら, 新しい道具使うときには不具合が発生しがちなので, 大切な実装を進める際にはある程度枯れた知見のある道具が安全です。
普通にFlutterとかReact使うのが安全ってことです。pythonと連携させるのが怠いのが難点ですが。


追伸
坂道だらけ地域に住んでて車持つにも微妙な都内住みだったので, 電動自転車買ったら最高でした。
なんで税金かからないのか分からない。1カ月くらい充電保つ, 70km走れるとか。
BE-FD631B ビビ・DX 26型(オニキスブラック)|ビビ・DX【パナソニック公式通販】

React-Native2秒で壊すで!"HelloWorld"含むプロジェクト名付けたらバグるぞ

React-Nativeのプロジェクト名に"HelloWorld"含む命名するとバグるで!!
以上!解散!

追伸
新しい枕でBrainSleep買ったら良い感じでした。風通し良くて涼しい。1週間使うと頭の形にfitするとか。枕形状直したい時は60℃の湯につけてたら形状初期化するらしい。長く使っててペシャッても直せるの良い。

洋風油揚げパスタ(冷蔵庫消費レシピ)

冷蔵庫余り物使いまくったら美味しくなったパスタです。

■材料 3人前
 油揚げ 2枚
 薄切りのお肉(豚でも牛でも) 100g程度
 ブラウンマッシュルーム 2-3個
 白ネギ 1本
 トマト 1個
 レモン 1/4個
 チェダーチーズ 適当
 ニンニク 2片
 オリーブオイル 適量
 コンソメ顆粒 2-3振り
 パスタ 3束
■レシピ
step1. フライパンにオリーブオイル引いて刻みニンニクを軽く炒めます。
step2. フライパンにネギとブラウンマッシュルーム入れて炒める。
step3. コンソメ顆粒と肉と油揚げ入れて炒める。
step3.5. 1.5L-2.0Lの水に塩一掴み入れて、沸騰させてパスタ茹でる。
step4. パスタをフライパンに入れて絡める。茹で汁入れて塩味調整。
step5. ええ感じに水気飛ばしてパスタと具材絡めたら、刻みトマトとチェダーチーズ乗せて、レモン絞って完成


追伸
天井にプロジェクター投影して寝ながらコンテンツ消費できる ダメ人間生成環境始めました。光量強いので昼間でも電気点けてても普通に見れる良い。
Linux OS 搭載】 WANBO TT プロジェクター 小型... https://www.amazon.jp/dp/B0BR7P7CW6?ref=ppx_pop_mob_ap_share

モンブランショートケーキ

モンブラン大好きマンです。自作すれば食べ放題です。
モンブラン作れるマンになって最強のエンジニアになりましょう。

材料(15cm型)
■スポンジ
 ・卵 2個
 ・砂糖 50-60g
 ・薄力粉 50-60g
 ・生クリーム 20g
■シロップ
 ・グラニュー糖 15gに水30-40g混ぜたら甘い水(シロップ)が出来ます。
モンブランクリーム
 ・マロンクリーム 240g
 ・牛乳 20g
 ・生クリーム 100g
■ケーキ全体の構成に使う材料
 ・生クリーム 280g
 ・砂糖 30g
 ・バニラエッセンス 2-3滴
 ・クリームチーズ 100g
 ・栗(渋皮煮とかシロップ漬けのやつ) 載せたいだけ

まずはスポンジ作ります。
■スポンジ

  • step1. ボウルに全卵2個 + 砂糖を入れて泡立てます。角が立たないで倒れる程度に泡立てます。
  • step2. 粉ふるいにかけた薄力粉をさっくりと混ぜ込みます。だまになると触感悪いので粉ふるい大事です。
  • step3. 生クリームを入れてさっくり混ぜます。生クリームは最後に入れてください。順番間違えるとスポンジ膨らまなくて悲しい思いをします。世界中から悲しみをなくしたいので, ご協力おねがいします。
  • step4. 170℃に予熱したオーブンで25-30分焼きます。オーブンの大きさによって時間調整してください。
  • step5. 網などで底を浮かせて冷やします。冷やさないと切る時に崩れて悲しい気持ちになることがあります。世界から悲しみをなくしたい。

スポンジ完成
モンブランクリーム

  • step6. マロンクリームと牛乳を混ぜます。
  • step7. 泡立てた生クリーム100g入れてさっくり混ぜます。ケーキ全体の仕上げに使う生クリームと一緒に泡立てて,そのボールから盗むと洗い物減って楽ちんです。

■ケーキ全体の構成

  • step8. 生クリームに砂糖とバニラエッセンス入れて泡立てます。氷水で冷やしながら立てると仕上がり良いです。
  • step9. クリームチーズ入れて泡立て機で混ぜ込みます。
  • step10. スポンジを5枚になるようにスライスします。3枚柔らかい部分を中抜きして使います。上下の焼き目付いた固い部分はおやつにしてください。焼き目のついた硬いスポンジを使うと, ケーキにナイフが通らず, スポンジ同士の隙間から生クリームが噴き出す悲劇が起こって, とてもとても悲しい思いをします。悲しくないケーキを作るためにご協力ください。
  • step11. スポンジ表面にシロップ(砂糖水)を薄く塗ります。霧吹きor刷毛を使ってください。クリームとスポンジがなじんで良い感じになります。
  • step12. スポンジ同士の間に先ほど作った好みのクリーム(マロンorプレーン)を塗ってください。分厚く塗り過ぎないように気を付けてください, step10に記述した悲劇の再臨になります。
  • step13. 栗をスポンジの間に生クリームと一緒に挟んだりしましょう。
  • step14. 余ったプレーンクリームをケーキ全体に塗ります。全体に塗る時はケーキナイフとか回転台があるととても綺麗に出来ます。
  • step15. マロンクリームをケーキの頂上に絞ってあげます。栗も載せてあげたら可愛いです。

参考レシピ↓ 動画付きで分かりやすいです。僕のレシピでは材料の一部をスーパーで買えるものに置き換えました。
モンブラン・ショートケーキの作り方 Mont Blanc Short Cake|HidaMari Cooking - YouTube

エンジニアのよだれ鶏

エンジニアのよだれ鶏

旨過ぎてビール飲みたいけど、冷蔵庫にないので諦めました。日が落ちても暑過ぎなので徒歩2分のコンビニに行くのも億劫です。

材料(4人前)
  ・鶏胸肉 500-600g
 漬けダレ(肉浸かる量、以下タレの混合比率)
  ・醤油 1
  ・みりん 1
  ・料理酒 1
 よだれ鶏ソース
  ・豆板醤 辛くしたいだけ
  ・ニンニク 2-3片
  ・生姜 チューブ1-2cmくらい
  ・お酢 大さじ3
  ・醤油 大さじ3
  ・みりん 大さじ1
  ・料理酒 大さじ1
  ・花山椒 4振りくらい

レシピ
鶏肉低温調理
 1. 鶏胸肉をジッパー袋に入れて、漬けダレに2-3時間以上漬けておきます。
 2. 沸騰したお湯に肉ジッパー入れて火を消して、1時間以上放置。
 3. 1時間で湯温が70-80℃(指先いれて3秒で火傷しそうなくらい)以下まで下がっているようなら、弱火で温度を80℃くらいまで上げて、追加で2時間以上放置。
 4. 3時間程度で鶏肉と湯温が70℃以上を維持してれば大まかに火が通ってると僕は判定します。
よだれ鶏のタレ
 5. フライパンに油引いて、ニンニク豆板醤ショウガを入れて香り良くなるまで弱火で炒めます。香味油完成。
 6. お酢、醤油、みりん、料理酒入れて軽く水分と酸味を飛ばします。
 7. 火を止めてから花山椒を入れて、花山椒をタレ全体に軽くなじませます。
盛り付け
 8. 鶏肉を割くorスライスしてタレかけて完成。

完全に旨くて完璧な優勝
約束された食欲 最高 旨過ぎ


鶏肉火が通ってないと食中毒エグい怖いです。深部温度を65℃以上15分以上にして必ず火を通しましょう。

あと肉スライスするなら冷蔵庫で冷やしてからがオススメです。
肉の油脂が冷えて固まるので、スライスする時に切りやすいです。熱々の肉は油脂が溶けてて、肉全体が柔らかくてスライスしにくいです。

エンジニアの麻婆豆腐〜野菜沢山 豆板醤なしver.〜

野菜買い過ぎたエンジニアの麻婆豆腐です。

野菜買い過ぎたけど豆板醤買ってなかったので、味噌で作ります。


f:id:werry-chan:20230708213059j:image

材料(4人前)

・冷凍豚ひき肉 200g

・豆腐 2-3丁

・玉ねぎ 半玉

・ナス 2本

・ピーマン 2個

 

・ニンニク 2−3片

・唐辛子 好きな量

・味噌 大さじ盛2

・片栗粉 大さじ盛1

・料理酒 大さじ2

・醤油 大さじ2

 

レシピ

1. フライパン油にニンニク, 唐辛子, 味噌を入れて弱火で香り良く炒めて香味油作ります。

2. ひき肉入れて軽く火を通します。

3. 玉ねぎ, ナス, ピーマン縦長で切って, フライパンに入れます。野菜が多くてフライパンから溢れそうなら, 蓋して5分くらい蒸して柔らかくします。

4. 料理酒と醤油入れて軽く炒めます。

5. 豆腐入れて軽く混ぜます。

6. 水100cc程度に溶いた片栗粉をフライパンに入れて手早く全体に行き渡るように混ぜます。

7. とろみついたら完成です。

 

 

validation(test)がtrainデータよりaccuracy高くなる問題, dropout見直してみませんか?

機械学習ある程度やっている人が稀に遭遇する現象
あれ, testデータの方がtrainデータよりaccuracy高くね?
なんか変じゃね?すごい不安, このままリリースして問題ないの?

この現象, もしかするとdropoutが原因かもしれません。

この記事の内容を要約すると,
「modelのtrain時とeval時でbatch normalizationやdropoutの挙動が異なることから, testの方がtrainよりaccuracyが高くなる現象が発生している場合があるため, 評価方法を見直してみましょう」です。


では実際に, この現象を再現してみましょう。
まずはデータセットを作ります。

# make easy dataset
import numpy as np
import torch


def make_easy_dataset(X_length):
    theta = np.linspace(0,50*np.pi, 5000)
    sin   = np.sin(theta)
    X, y  = [], []
    for i in range(len(theta) - X_length - 1):
        X.append([sin[i : i + X_length]])
        label = [0,1]
        if sin[i + X_length + 1] > sin[i + X_length]:
            label = [1,0]
        y.append(label)
    return np.array(X, dtype=np.float32), np.array(y, dtype=np.float32)


class MyEasyDataset(torch.utils.data.Dataset):

    def __init__(self, data, label, transform=None):
        self.transform = transform
        self.data_num  = data.shape[0]
        self.data      = torch.tensor(data)
        self.label     = torch.tensor(label)
    
    def __len__(self):
        return self.data_num

    def __getitem__(self, idx):
        out_data  = self.data [idx]
        out_label = self.label[idx]
        if self.transform:
            out_data = self.transform(out_data)
        return out_data, out_label

inputはsin波, prediction対象は sin波の値が次に上昇するか否かです。

次はmodelを定義します。
簡単な1次元Convolutionを任意層重ねたもの, 簡単なFullConnect層を任意層重ねたもの, 複数のモデルを束ねるものです。

# DNN_module.py
import torch
from   torch import nn


class SimpleCNN1d(nn.Module):
    def __init__(self, in_channel:int, out_channel:int, num_block:int, kernel_size:int, stride:int=1, dropout:float=0.0):
        super(SimpleCNN1d, self).__init__()
        self.in_channel  = in_channel
        self.out_channel = out_channel
        self.num_block   = num_block
        self.kernel_size = kernel_size
        self.dropout     = dropout
        layers = []
        for i in range(num_block):
            if i != 0:
                layers += [nn.Conv1d(out_channel, out_channel, kernel_size, stride), nn.ReLU()]
            else:
                layers += [nn.Conv1d( in_channel, out_channel, kernel_size, stride), nn.ReLU()]
            if dropout > 0:
                layers += [nn.Dropout(dropout)]
        self.net = nn.Sequential(*layers)
    def forward(self, x):
        return self.net(x)


class FC_classifier(nn.Module):
    def __init__(self, in_channel:int, in_length:int, out_channel:int, num_block:int, dropout:float=0.0):
        super(FC_classifier, self).__init__()
        self.in_dim      = in_channel * in_length
        self.out_channel = out_channel
        self.num_block   = num_block
        self.flatten     = nn.Flatten()
        layers = []
        for i in range(num_block):
            in_dim_  = self.in_dim // (i + 1)
            if in_dim_ < 1:
                in_dim_ = 1
            out_dim_ = in_dim_ // 2
            if out_dim_ < out_channel or i == num_block-1:
                out_dim_ = out_channel
            layers += [nn.Linear(self.in_dim, out_dim_)]
            if dropout > 0:
                layers += [nn.Dropout(dropout)]

        layers += [nn.Hardsigmoid()]
        self.net = nn.Sequential(*layers)
    def forward(self, x):
        x = self.flatten(x)
        out = self.net(x)
        return out


class CombinedNet(nn.Module):
    def __init__(self, models):
        super(CombinedNet, self).__init__()
        layers = []
        for model in models:
            layers += [model]
        self.net = nn.Sequential(*layers)
    def forward(self, x):
        return self.net(x)

このネットワークを組み合わせて, 実際に学習してみましょう。

dropoutを有効にして, 実際にvalidation(test) accuracyの方がtrain accuracyより大きくなる現象を再現しましょう。

import torch
from sklearn.model_selection import train_test_split
from matplotlib import pyplot as plt

import make_easy_dataset as med
import DNN_module


def accuracy(logits, correct_logits):
    indices_max     = logits.max(dim=1).indices
    indices_correct = correct_logits.max(dim=1).indices
    return (indices_max == indices_correct).sum() / logits.shape[0]

def train(dataloader, model, loss_func, optimizer, eval_mode=False):
    if eval_mode:
        model.eval()
    else:
        model.train()
    ave_loss, num_batch, ave_acc = 0, 0, 0
    for data in dataloader:
        X, y = data
        out  = model(X.to(device))
        loss = loss_func(out, y.to(device))
        acc  = accuracy(out, y.to(device))
        
        if not eval_mode:
            loss.backward()
            optimizer.step()

        ave_loss  += loss
        ave_acc   += acc
        num_batch += 1
    ave_loss /= num_batch
    ave_acc  /= num_batch
    if eval_mode:
        model.train()
    return float(ave_loss), float(ave_acc)


if __name__ == "__main__":
    device = torch.device('cuda:0') if torch.cuda.is_available() else torch.device('cpu')
    print(device)

    data_len    = 150
    X, y        = med.make_easy_dataset(data_len)
    X_train, X_valid, y_train, y_valid = train_test_split(X, y, stratify=y, train_size=0.5)
    dataset_train = med.MyEasyDataset(X_train, y_train)
    dataset_valid = med.MyEasyDataset(X_valid, y_valid)
    dataloader_train  = torch.utils.data.DataLoader(dataset_train, batch_size=256)
    dataloader_valid  = torch.utils.data.DataLoader(dataset_valid, batch_size=256)


    num_block   = 2
    kernel_size = 10
    base_model        = DNN_module.SimpleCNN1d(  in_channel = 1,
                                                out_channel = 1,
                                                num_block   = num_block,
                                                kernel_size = kernel_size,
                                                dropout     = 0.5,
                                            )

    base_out_data_len = data_len - (kernel_size - 1)*num_block
    classifier        = DNN_module.FC_classifier( in_channel  = 1,
                                                in_length   = base_out_data_len,
                                                out_channel = 2,
                                                num_block   = 1,
                                                dropout     = 0.5,
                                                )

    combined_model    = DNN_module.CombinedNet([base_model, classifier]).to(device)

    optimizer = torch.optim.Adam(combined_model.parameters(), lr=1e-4)

    loss_func = torch.nn.BCELoss().to(device)


    histry_train_loss, histry_train_eval_loss, histry_valid_loss = [], [], []
    histry_train_acc , histry_train_eval_acc , histry_valid_acc  = [], [], []

    epochs = 100
    for t in range(epochs):
        loss_train     , acc_train      = train(dataloader_train, combined_model, loss_func, optimizer)
        loss_train_eval, acc_train_eval = train(dataloader_train, combined_model, loss_func, optimizer, eval_mode=True)
        loss_valid     , acc_valid      = train(dataloader_valid, combined_model, loss_func, optimizer, eval_mode=True)
        histry_train_acc      .append(acc_train)
        histry_train_eval_acc .append(acc_train_eval)
        histry_valid_acc      .append(acc_valid)
        histry_train_loss     .append(loss_train)
        histry_train_eval_loss.append(loss_train_eval)
        histry_valid_loss     .append(loss_valid)
    plt.plot(histry_train_acc     , color='blue'   , label='train acc')
    plt.plot(histry_valid_acc     , color='orange' , label='valid acc')
    plt.plot(histry_train_eval_acc, color='skyblue', label='train eval acc')
    plt.legend(); plt.ylim(0,1)
    plt.savefig('acc_histry.png'); plt.clf(); plt.close()

    plt.plot(histry_train_loss     , color='blue'   , label='train loss')
    plt.plot(histry_valid_loss     , color='orange' , label='valid loss')
    plt.plot(histry_train_eval_loss, color='skyblue', label='train eval loss')
    plt.legend()
    plt.savefig('loss_histry.png'); plt.clf(); plt.close()

学習結果を以下に示しました。
通常のtrain(model.train()でmodel(train_data)) : 青色
valid(model.eval()でmodel(valid_data)):オレンジ色
eval modeでtrain(model.eval()でmodel(train_data)):水色

epoch毎のaccuracy, train:青, valid: オレンジ, eval modeのtrain: 水色

見た目から簡単に分かることとして, 青線だけは水色・オレンジ色とは明らかに異なっています。

青線のみはmodel.train()のtrain modeに入力された結果, 水色・オレンジ色はmodel.eval()のeval modeに入力された結果です。

model.eval()モードという同じ基準下で, 水色(train data)とオレンジ色(valid data)を比較すると, 乖離ない学習曲線が見えます。

同様の現象がLossにも確認できます。

epoch毎のloss, train:青, valid: オレンジ, eval modeのtrain: 水色

さて, この現象はどうして発生したのか?


理由は, model.train()においてはdropoutが有効で, model.eval()ではdropoutが無効化されているということが原因として挙げられます。

dropoutは, networkの重みを確率的に無視するという機能を持ちます。
dropout値が大きすぎる, あるいは重要なlayerの重みを無視してしまった場合, model.eval()と出力が大きく異なるという現象が発生します。

このような現象に遭遇した場合には, 学習時間が倍程度になりますが, model.train()で学習した後に, model.eval()でdropout無効化してtrainデータを再評価することが必要です。

通常は, 学習時間が2倍になることから, このような評価方法は実施しません。
また, dropoutが有効であっても, 他のnetworkや残った重みがカバーしてくれるため, 一般的なdropout値であれば問題になることは少ないです。


どうしてもtestデータ validationデータがtrain accuracyよりも高いことが不安で, この問題を修正しないと製品リリース許可出なそう, みたいな時には, 是非ともお試しください。