LoginSignup
1
0

More than 3 years have passed since last update.

DeepRunning ~Level8~

Last updated at Posted at 2020-08-23

Level8.深層学習 DAY4

8-1.Section1) TensorFlowの実装演習

  ●TensorFlowについて
   •実務ではTensorFlowを活用することが多く、スクラッチで実装することは少ない。
   •TensorFlow、Keras、Pytorchの順。
   •TensorFlowはgoogleが作ったディープラーニングのフレームワークで、
    調べる際もTensorFlowが多い。

  ●実装「constant」
   •TensorFlow ではテンソルというものを利用する。
   •Session.run() で起動しないと中身も確認できない。

Day4_0001tensorflow.png

  ●実装「placeholder」
   ・placeholderは、箱のようなものを用意している。
   ・placeholderは後から値を自在に変更できる。
   ・feed_dictで配列のx番目の要素を渡す。
    ⇒xをバッチに渡して実行するときなどに使用する。

Day4_0002tensorflow.png

  ●実装「variables」
   ・a = tf.constant(10)と定義するとaに10を設定する。
   ・x = tf.Variable(1)後々に値を変えて更新できる。
    ⇒「calc_op = x * a」「update_x = tf.assign(x, calc_op)」
     で値を更新している。
   ・init = tf.global_variables_initializer()は、
    変数を初期化する。

Day4_0003tensorflow.png

8-1-2 線形回帰の実装演習

  ●matplotlibのjitterの仕様(1度目にプロットできない)
   matplotlibの下にinlineを書いてあげる。

  ●コード
   ・iters_num ・・・何回学習するか。
   ・plot_interval ・・・10回ごとに結果を表示する。
   ・n = 100
    x = np.random.rand(n)
    d = 3 * x + 2 ・・・100個のランダムなxを用意し、3*x+2の値を作る。
   ・xは直線の値のため、noiseを加える。
    ⇒上下にまばらな値ができる。
   ・train = optimizer.minimize(loss) ・・・誤差を最小化する学習。
   ・loss = tf.reduce_mean(tf.square(y - dt)) ・・・回帰二乗誤差
   ・sess.run(train, feed_dict={xt:x_train,dt:d_train})
    ・・・trainを読み出し、feed_dictでx_train、d_trainを渡していく。
   ・W_val:[3.0194452]、b_val:[1.9318823]となっており、
    d = 3 * x + 2のW:3とb:2に近い予測値になっている。

  ●Tryの確認
   ・noiseの値を変更しよう
   ・dの数値を変更しよう

   noiseに0.55、bに9を設定する。
   ⇒W:[3.0428321]、b:[8.961408]と予測された。
    その他変更して試したが、noiseが大きいと上手く回帰できない。

Day4_0004tensorflow.png
Day4_0005tensorflow.png
Day4_0006tensorflow.png
Day4_0007tensorflow_tyr.png

  ☆考察☆
   •noiseによるデータの作成で、直線上からどの程度ノイズを発生させるのか、
   実際のデータがない時に一般データとの差を考えるのも難しいと感じた。
   •Wとdがきちんと予測されると面白い。

8-1-3 非線形回帰の実装演習

  ●非線形な一直線でない式を予測する。

  ●コード
   ・「d = -0.4 * x ** 3 + 1.6 * x ** 2 - 2.8 * x + 1」
    非線形な解を作成している。
   ・Wが-0.4、1.6、-2.8、1と4つあるため、
    WのVariableのoshapeを[4,1]と定義。
   ・optimizer = tf.train.AdamOptimizer(0.001)に変更している。
   ・[[-0.3990355]
     [ 1.6032437]
    [-2.8051274]
    [ 1.0001277]] ・・・4つのWがきちんと予測できている。

  ●Tryの確認
   ・noiseの値を変更しよう
   ・dの数値を変更しよう

   noiseを0.35にしてみた。
   ⇒[[-0.36423665]
    [ 1.6244664 ]
    [-2.8141403 ]
     [ 0.96930456]] ・・・4つのWはほぼ予測ができている。
Day4_0008tensorflow_tyr.png
Day4_0009tensorflow_tyr.png

   dを変更してみる。
   「d = 0.4 * x ** 3 + 2.8 * x ** 2 - 2.8 * x + 10」
   ⇒[[ 0.31515315]
     [ 3.8476086 ]
    [-2.596497 ]
     [ 7.5793443 ]] ・・・ちょっと厳しかったようだ。
Day4_0010tensorflow_tyr.png

  ☆考察☆
   •noiseを大きくしても線形回帰と比べてあまり分散しなくなるようだ。
   •dは線形回帰と比べても極端に予測がズレない。4つのWで複雑な非線形になると、
   平均二乗誤差の収束は難しくなるが、線形回帰より大きく外れないと思った。

  ●Tryの確認
   次の式をモデルとして回帰を行おう
   $𝑦=30𝑥^2+0.5𝑥+0.2$

   誤差が収束するようiters_numやlearning_rateを調整しよう
Day4_0011tensorflow_tyr.png
Day4_0012tensorflow_tyr.png
Day4_0013tensorflow_tyr.png

  ☆考察☆
   ・iters_numを30000~35000回くらいに設定しないと収束しなかった。
    データ作成数を増やすなど、様々な試行錯誤は必要だと感じた。
    「optimizer = tf.train.AdamOptimizer(0.01)」を0.001⇒0.01にする差が大きい。
    ⇒iters_numは25000回くらいで十分だった。

8-1-4 分類1層(mnist)

  ●MNISTの分類
    手書き文字のデータセットで、28×28×白黒データで1チャンネル。
    0~9のどの数字かを予測する。

  ●Tryの確認
   次の式をモデルとして回帰を行おう
   ・x:入力値, d:教師データ, W:重み, b:バイアス をそれぞれ定義しよう
Day4_0014tensorflow_MNIST_try.png
Day4_0015tensorflow_MNIST_try.png

   ・内容を確認する。
    ①x_batch[0]
    ②d_batch[0]
    ③x_batch[0].shape
    ④plt.imshow(x_batch[0].reshape(28,28))
Day4_0016tensorflow_MNIST_try.png
Day4_0017tensorflow_MNIST_try.png
Day4_0018tensorflow_MNIST_try.png
Day4_0019tensorflow_MNIST_try.png

  ☆考察☆
   詳細はもう少し復習して深堀してみるが、
   1層で出力のsoftmax関数が出力する0~9の分類が87%の精度はすごいなと感じた。

8-1-5 分類3層(mnist)

  ●MNIST1層より精度を上げる。
   ・hidden_layerを2つ用意する。
   ・784(入力)⇒600、600⇒300⇒10(個の出力)のニューラルネットワーク
   ・隠れ層があるのでWやbを3つずつ用意。
   ・TensorFlowでは毎回Variableを定義する必要がある。
   ・Wが3つある場合:W1,W2,W3と、b1,b2,b3と定義する。
   ・出力の前にドロップアウトしており、汎化性能を上げるため、
    あるニューロンを消している。
    「drop = tf.nn.dropout(z2, keep_prob)」

  ●Tryの確認
   隠れ層のサイズを変更してみよう(600、300)
   optimizerを変更しよう
Day4_0020tensorflow_MNIST_try.png
Day4_0021tensorflow_MNIST_try.png
Day4_0022tensorflow_MNIST_try.png
Day4_0023tensorflow_MNIST_try.png
Day4_0024tensorflow_MNIST_try.png
Day4_0025tensorflow_MNIST_try.png

  ☆考察☆
   修正前の状態で90%となった。dropout_rate=0としたら91%となったが、
   過学習になってしまってのかもしれない。
   隠れ層のサイズを半分にしたところ、87%と精度が落ちた。
   逆に800、500と増やすと91%と少し向上する。増やせばいいものだろうか。
   ⇒増やすと学習に時間がかかるようになった。
   optimizerは、AdaGradを試してみた。
   最終的な精度は90%だが、途中からの伸びが良かった。
   講義と同じように全種類試した。
Day4_0026tensorflow_MNIST_try.png

8-1-6 分類CNN(mnist)

  ●Conb×pool×Conb×poolで、
   affin×dropout×affinという構造。
   ・1層目でreshapeにより28×28×1チャンネルの画像として処理。
   ・W_conb1は5×5×1×32というウェイト
    5×5:CNNのフィルターサイズ
    1×32:1チャンネルのものを32チャンネルに拡張する
   ・padding='SAME':shapeが変わらないようなpaddingを用意する。

  ●Tryの確認
   ドロップアウト率を0に変更しよう
Day4_0027tensorflow_MNIST_try.png
Day4_0028tensorflow_MNIST_try.png
Day4_0029tensorflow_MNIST_try.png
Day4_0030tensorflow_MNIST_try.png

  ☆考察☆
   ドロップアウト率が0.5の時は、93%の精度だった。
   ドロップアウト率を0にしたら、精度が91%と落ちた。
   ただ、90~96%をふらふらしており、学習モデルとしては安定していない印象。
   x_batchでトレーニングデータを渡したもので、汎化性能は不明。
   テストデータの場合、ドロップアウトさせた方が精度があがる。

8-1-7 論文から実装する

  ●画像認識の最新モデルを実装するには?
   ・VGG
   ・AlexNet
   ・GoogLeNet
   ・Resnet
   ・Yolo

   ⇒・VGG16でarxivと検索するとarxivのサイトにて論文を参照でき、
     サイトから無料で手に入る。
    ・ABSTRACTを読む→ARCHITECTUREを見る。
     RESULTで精度がどうなったか確認する。
    ・表とかテキストで実装した組み合わせなどがまとめられている。
    ・VGGはブログの記事になっているものもある。
    ・コードがGitHubに載っているので参考にする。
    ・AlexNet等でもアーカイブがあり、構成図を参照したりする。
     「pytorch AlexNet」のように検索する。
    ・ResnetはDeep Residual Learning for Image Recognitionで、
     他と同じようにアーカイブがある。
    ・最新のものでなければ、既にあるものから実装する。
    ・物体認識(Yolo)もPDFで提供されているが、
     再現実装するのは大変だったりする。

8-1-8 例題・例題解説

  ●例題1つ目
   解説なし
Day4_0031_TEST.png

  ●例題2つ目
   正解は(a)
Day4_0032_TEST.png

  ●例題3つ目
   正解は(a)
Day4_0033_TEST.png
   
  ●例題4つ目
   正解はあ:(a)、い:(a)、う:(a)
Day4_0034_TEST.png
Day4_0035_TEST.png
Day4_0036_TEST.png

  ●例題5目
   正解は(あ)
Day4_0037_TEST.png

  ●例題6目
   正解は(a)
Day4_0038_TEST.png

☆確認テスト☆
  ・8-1-8-1 VGG・GoogLeNet・ResNetの特徴をそれぞれ簡潔に述べよ。

  【自分の回答】
    VGG:16層と19層の構造のバージョンがある。勾配消失問題がある。
    GoogLeNet:22層の構造。Inceptionモジュールが特徴。
    ResNet:152層の構造。VGGの勾配消失問題を解決。

  【解答】
    VGG:最も古くて2014年のモデル。Convolution、Convolution、max_poolという
       単純なネットワークの積み重ねで出来ている。パラメータ数が多い。
    GoogLeNet:Inception moduleを使ているのが特徴。1×1の畳み込みの次元削減。
    ResNet:スキップコネクションアイデンティモジュールを使うことで残差接続を行い、
        深い学習ができるという特徴。

8-1-9 Keras1

  ●Kerasについて
   •TensorFlowのラッパーである。
   •初心者でも簡単に記述できる。
   •複雑なことを行う場合、TensorFlowを書かなければいけない。

  ●実装実習「線形回帰」
Day4_0039_Keras.png
Day4_0040_Keras.png
Day4_0041_Keras.png

  ●実装実習「単純パーセプトロン」
Day4_0042_Keras.png
Day4_0043_Keras.png
Day4_0044_Keras.png

  ●Tryの確認
   np.random.seed(0)をnp.random.seed(1)に変更
   エポック数を100に変更
   AND回路, XOR回路に変更
   OR回路にしてバッチサイズを10に変更
   エポック数を300に変更しよう
Day4_0045_Keras_try.png
Day4_0046_Keras_try.png
Day4_0047_Keras_try.png
Day4_0048_Keras_try.png
Day4_0049_Keras_try.png
Day4_0050_Keras_try.png
Day4_0051_Keras_try.png
Day4_0052_Keras_try.png
Day4_0053_Keras_try.png
Day4_0054_Keras_try.png
Day4_0055_Keras_try.png
Day4_0056_Keras_try.png

  ☆考察☆
   単純なOR回路でもバッチサイズやエポック数によっては、
   正解率が100%にならないことがある。XOR回路は、
   ハイパーパラメータを変更してもロスが減らず不正解がある。
   OR回路は非線形のため、1層ではうまくいかない。

  ●実装実習「分類(iris)」

  ●Tryの確認
   中間層の活性関数をsigmoidに変更しよう
   SGDをimportしoptimizerをSGD(lr=0.1)に変更しよう

   ソースの修正が必要だった。
   # from sklearn.cross_validation import train_test_split
   ⇒from sklearn.model_selection import train_test_split

   # plt.plot(history.history['acc'])
   # plt.plot(history.history['val_acc'])
   ⇒plt.plot(history.history['accuracy'])
   ⇒plt.plot(history.history['val_accuracy'])
Day4_0057_Keras_try.png
Day4_0058_Keras_try.png
Day4_0059_Keras_try.png
Day4_0060_Keras_try.png
Day4_0061_Keras_try.png
Day4_0062_Keras_try.png

  ☆考察☆
   sigmoid関数だと勾配消失を起こすので、
   ReLU関数の汎化性の良さはわかるが、irisデータだと分かりづらい。
   複雑なデータを扱った際に、それぞれ検証してみようと思う。

8-1-10 Keras1

  ●実装実習「分類(mnist)」

  ●Tryの確認
   load_mnistのone_hot_labelをFalseに変更しよう (error)
   誤差関数をsparse_categorical_crossentropyに変更しよう
   Adamの引数の値を変更しよう
Day4_0063_Keras.png
Day4_0064_Keras.png
Day4_0065_Keras.png

  ☆考察☆
   model.compileの指定方を修正しないと、上手く動作しなかった。
   コーディングのいけないところは、じっくり再確認する予定。
   one_hot_labelの設定と、誤差関数をsparse_categorical_crossentropy、
   categorical_crossentropyの指定方については間違えないようにする。

  ●実装実習「CNN分類(mnist)」

  ●Tryの確認
   実行に時間がかかるため割愛
Day4_0066_Keras.png
Day4_0067_Keras.png
Day4_0068_Keras.png

  ☆考察☆
   確かに処理に時間かかることが確認できた。
   padding=SAMEした時の動作比較検証は地道だが面白そう。
   多層のconvolutionについてもサイズを変更して確認してみる。

  ●実装実習「cifar10」

  ●Tryの確認
   実行に時間がかかるため割愛
Day4_0069_Keras.png
Day4_0070_Keras.png
Day4_0071_Keras.png

  ☆考察☆
   50000枚のトレーニングデータ、10000枚のテストデータで、
   カラーの画像をRGBのため255で正規化している。
   割愛であるが、処理時間とコードの内容確認のため実施してみた。
   動画のように枚数を絞って実行すればよかった・・・。

  ●実装実習「RNN」
   2進数足し算の予測
   Keras RNNのドキュメント https://keras.io/ja/layers/recurrent/#simplernn

  ●Tryの確認
   RNNの出力ノード数を128に変更
   RNNの出力活性化関数を sigmoid に変更
   RNNの出力活性化関数を tanh に変更
   最適化方法をadamに変更
   RNNの入力 Dropout を0.5に設定
   RNNの再帰 Dropout を0.3に設定
   RNNのunrollをTrueに設定
Day4_0072_Keras.png
Day4_0073_Keras.png
Day4_0074_Keras.png
Day4_0075_Keras.png
Day4_0076_Keras_try.png
Day4_0077_Keras_try.png
Day4_0078_Keras_try.png
Day4_0079_Keras_try.png
Day4_0080_Keras_try.png
Day4_0081_Keras_try.png
Day4_0082_Keras_try.png
Day4_0083_Keras_try.png
Day4_0084_Keras_try.png
Day4_0085_Keras_try.png

  ☆考察☆
   ※BGMがうるさ過ぎて解説の声がすごく聞きづらい。。。
   KerasでRNNの実装はすごく楽である。
   sigmoidは精度が出ない。状況に応じて使い分けるのは計算させてみてだろうか。
   RNNの出力活性化関数をtanhにしたところ、直ぐに収束し精度が良い。
   KerasならsimpleRNNから様々なRNNのネットワークに切替えが可能。
   Kerasは容易に扱える印象であるが、やはりTensorflowで組めるようにしておきたい。

8-2.Section1) 強化学習

8-2-1 強化学習とは

  ●強化学習とは
   長期的に報酬を最大化できるように環境の中で行動を選択
   できるエージェントを作ることを目標とする機械学習の一分野。
   ⇒行動の結果として与えられる利益(報酬)により、
    行動を決定する原理を改善していく仕組み。
    教師あり学習や教師なし学習とは違った分野。

   ・マーケティングの応用例
    プロフィールと購入履歴からキャンペーンメールを顧客に送付するソフト。
    行動:顧客ごとに送信、非送信の行動を選択。
    報酬:キャンペーンのコスト(負の報酬)、
       生み出されると推測される売り上げ(正の報酬)を受ける。

☆確認テスト☆
  ・8-2-1-1 強化学習に応用できそうな事例を考え、
       環境・エージェント・行動・報酬を具体的に挙げよ。

  【自分の回答】
   環境:コンビニ等の共通したポイントカード導入箇所
   エージェント:ポイントの蓄積・ポイント利用の履歴により、
          利用者還元セールやポイント10倍Dayなどを開催する。
   行動:顧客のポイントカード利用履歴から、ポイントを還元する、
      通常よりポイントを上乗せしてもよいか選択する。
   報酬:ポイント還元・上乗せによる負の報酬と、
      還元セールや上乗せにより購入が増えると推測される正の報酬。

  【解答(例)】
   ゲームやボードゲーム
   プレイヤーが指す手についての強化学習

8-2-2 探索と利用のトレードオフ

  ●探索と利用のトレードオフ
   環境について事前に完璧な知識があれば、
   最適な行動を予測し、決定することは可能である。

   ⇒どの顧客にキャンペーンメールを送付すると、どの行動するか既知である。
    (そんなことはそうそうない。)

   ⇒強化学習の場合、そのような仮定は成り立たないとする。
    不完全な知識を元に行動しながらデータ収集し、最適な行動を探索する。

  ●問題になるのは・・・
   探索と利用のトレードオフである。

   「過去のデータで、ベストとされる行動のみを常に取り続ければ、
    他にもっとベストな行動を見つけることができない。」
    (探索が足りない状態)
       ↑
     トレードオフの関係性
       ↓
   「既知の行動のみ常に続ければ、過去の経験が活かせない」
    (利用が足りない状態)

   どちらの状態も良くなく、トレードオフを上手く調整することが必要。

8-2-3 強化学習のイメージ

  ●強化学習のイメージ
   エージェントがある方策を実施して行動した状態sとなる。
    ⇒それを観測し、状態sの報酬価値を受け取る。
   という流れ。
Day4_0086_reinforcementlearning.png
Day4_0087_reinforcementlearning.png

  ●関数
   方策:方策関数 Π(s,a)で表される。
   行動や価値V:行動価値関数 Q(s,a)で表される。

8-2-4 強化学習の差分

  ●強化学習と、通常の教師あり/教師なし学習との違い

   【結論】目標が違う
    ・教師あり/なし学習では、データに含まれるパターンを見つけ出す、
     及び、そのデータから予測することが目標
    ・強化学習では、優れた方策を見つけることが目標

8-2-5 強化学習の歴史

  ●強化学習について
   冬の時代があったが、計算速度の進展により大規模な状態を持つ場合でも、
   強化学習を可能としつつある。⇒ AlphaGo など

  ●Q学習
   行動価値関数を、行動するごとに更新することで学習を進める方法

  ●関数近似法
   価値関数や方策関数を関数近似する手法のこと

8-2-6 行動価値関数

  ●行動価値関数とは
   価値を表す関数としては、状態価値関数と行動価値関数の2種類がある。

   ⇒ある状態の価値に注目:状態価値関数
    状態と価値を組み合わせた価値に注目:行動価値関数

  ●Q学習
   行動価値関数を行動のたびに更新することで学習している。

8-2-7 方策関数

  ●方策関数とは
   方策ベースの強化学習手法において、
   ある状態でどのような行動を採るのかの価値を与える関数のこと。

   ⇒この状態であれば、この行動をとる確率が◎◎%といったもの。

8-2-8 方策勾配法

  ●方策反復法
   方策をモデル化して、最適化する手法
   $θ^{(t+1)}=θ^{(t)}+ε∇J(θ)$

  ●Jとは
   方策の良さである。
   ⇒定義しなければならない。

  ●定義方法
   ・平均報酬:行動をとった時に生まれる価値全部の平均
   ・割引報酬和:カッコになればなるほど報酬の加算する割合を減らす(減衰)

   ⇒定義に対応して、行動価値関数 Q(s,a)の定義を行い、
    方策勾配定理が成り立つ。

$∇_θJ(θ)=\mathbb{E}_{π_θ}[(∇_θlogπ_θ(a|s)Q^π(s,a))]$

   ⇒Q関数を使って上記のように定義している。

  ●例題解説
Day4_0088_reinforcementlearning.png
   正解は(a)
   方策をアップデートするのが方策勾配に基づくアルゴリズムである。

   方策勾配定理は基本的には下記の2式から導出される。
   ・状態価値関数 v(s) = sum_a (π(a|s)Q(s,a))
   ・ベルマン方程式 Q(s,a) = sum_s’(P(s’|s,a)[r(s,a,s’) + γV(s’)]

8-2-9 論文解説DCGAN

  ●DCGAN
   ・Deep Convolutional Generative Adversarial Networksという名前。
   ・CNNのディープなネットワークを使って敵対的学習により画像を生成する。
   ・例として、実際に存在しないベッドルームの画像をきれいに出力している。
   ・潜在空間でベクトルとして表現したものからGenerator部分の画像を生成するため、
    「笑顔の女性の顔」から「普通の女性の顔」を引いて、「男性の顔」を足し合わせる。
    ⇒「笑顔の男性の顔」を数式的に扱うことができる。
   ・Generatorの生成部分はランダムな100個の正規分布の値を使って画像生成している。
    ⇒生成画像と実際の画像を比較し、Discriminatorというモデルが、
     その画像が作られたものかを判定する。

  ●例題演習1
Day4_0089_reinforcementlearning.png
   正解は(a)0.5

  ●例題演習2
Day4_0090_reinforcementlearning.png
   正解は(a)

  ●例題演習3
Day4_0091_reinforcementlearning.png
   正解は(a)

8-2-10 Kaggleについて

  ●Kaggle
   ・データ分析のコンペティション
   ・データ分析・AIにおいての世界大会のようなものである。
   ・Googleが運営している。
   ・企業等が賞金を出している。
   ・日本人でも上位に入るチームがある。
   ・Kaggleへの登録方法。サイトに行けば登録できる。
   ・コンテスト参加迄の一連の流れ
   ・Researchが研究目的。
   ・Featuredが賞金があるコンテスト。
   ・titanicやmnistなどの初心者向けのコンペがある。
    (データセットが有名なため、不正が発生?順位があてにならない)
   ・Kernelsで世界のデータサイエンティストの実装を参照できる。
   ・参加した後の流れ
    よさそうなスコアのものや説明を読んだり、コピーして動かしてみたり、
    データの水増しのやり方を覚えたりすると良い。
   ・コンペではDiscussionもあるので、知見としてアイディアが共有される。
    有意義な議論があり、とても参考になる。
    KernelやDiscussionを見てみると良い。
   ・失敗談
    どのコンテストに出るかが難しい。
    ⇒データセットとタスクの見極め等
     データセットの容量が少ないものに出てみるとか、
     mnistなどGPUを必要としないものに出てみる等。
    Gitを使えれば良いが、スクリプトの管理が難しい。
    管理がごちゃごちゃになる。
    ⇒適当に管理しているといけないのでフォルダで管理する。
   ・自分一人で頑張ろうとして順位が上がらず、モチベーション低下する。
    Kernelを使いこなして参加することで維持できる。
   ・上位入賞するには、特徴量のパターンが出てきた時に、
    1000個と5000個では5000個の方が良い精度が出るので、
    時間があれば実践を増やすといい。
    シングルモデルは難しく、多数のモデルでアンサンブルが良い。
    色々な特徴量を用意して、精度を上げていく。
    ⇒最後にアンサンブルで多様性を出す。

8-2-11 実践につながるインプット

  ●学習のやり方
   プログラミングの習得⇒機械学習
   スクリプトベースの学習
   Kaggleでの実践⇒実務に活かす。
   論文の読み込み。
   データ分析やAIはKaggleが良い。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0