JetBotのroad_followingを使いやすくする。
この記事について
この記事はAI RC Car アドベントカレンダー17日目の記事です。この記事ではJetBotのroad_followingのデモを実行しやすくするTipsを紹介します。
JetBotのroad_followingのデモ
JetBotの公式リポジトリに置いてあるJetBotのデモはゲームコントローラを使い、ブラウザ上に表示された画像上の座標を入力することでアノテーションを行なっていきます。このままでももちろんデモとして成功します。しかし、ゲームコントローラで画像上の狙った場所にカーソールを持っていくのは至難の業です。さらに、ゲームパッドで入力された座標を逐次Pythonのインタプリタで処理しているため、ブラウザとJetBotが通信を行なっています。そのため、通信環境が著しく悪くなる走行会や展示会場では問題になります。
JetRacerのinteractive_regressionを利用する
interactive_regressionはJetRacer用のPythonノートブックですが、学習させる内容は全くな同じため、JetBotにも利用することができます。interactive_regressionは画像をWEBブラウザ上でのクリックであつけることができるため、簡単に画像のアノテーションを行うことが可能です。さらに学習とバリデーションを同じノートブック上で行えるため、走行前に、コース上で学習が足りていな部分を簡単に見つけることができます。
JetBot向けinteractive_regression
JetBot向けにinteractive_regressionを修正するのはとても簡単にです。JetRacerとの差分はカメラモジュールの呼出部分のみです。カメラオブジェクトの呼びだし箇所を変更すれば大丈夫です。さらに3セル目で呼び出されているfrom jetcam.utils import bgr8_to_jpeg
は不要なため、コメントアウトしておきます。具体的にはオリジナルのinteracitivi_regressionの最初のセルを以下のように書き換えます。
from jetbot import Camera, bgr8_to_jpeg camera = Camera(width=224, height=224) camera.start()
jupyter_clickable_image_widgetをインストールする必要があります。JetBotにログイン後、以下のGitHubリポジトリのREADMEを参照し、インストールを行います。
インストール後はJetRacerと同様にデータ収集と学習を行えば大丈夫です。JetRacerの学習の練習にもなります。
まとめ
Jupyterノートブック上で画像クリックによるアノテーションから学習までできるのは非常に楽で良いです。jupyter_clickable_image_widgetは便利なので、自前のプロジェクトでも積極的に活用していきたいです。
AI RC Carは何を見て走行しているのか?
この記事について
この記事ではJetBotやJetRacerで学習させたAIモデルが推論結果を出力する際に、画像上のどこに着目したかを調べます。手法としてはResNet-18のモデルにGrad-CAMを使って推論時に重視した画像上の箇所を可視化させます。
AIの推論とその根拠について
ディープラーニングの推論結果に対して、なぜその出力が出たのか、入力データをどう解釈したかを調べることはAI研究のホットキーワドとなっているようです。詳細はXAIなどで検索されると多くの研究に当たります。AI RC Carにおいても、走行時にステアリング角や、画像座標を出力しますが、いったい画像上の何を見て出力しているかわかりません。もし推論の結果とともに、推論結果が画像上のどの部分に着目したのかを可視化できれば、学習後のモデルがどのくらい走行中の状況をしっかり見ているかがわかります。
Grad-CAM
Grad-CAMはCNNの推論根拠を可視化する手法の一つです。詳しくは詳細に書かれば他の記事を参考いただければと思います。端的に言うと、推論結果出力後、その誤差逆伝播を遡り、変化の激しい(勾配の大きい)パーセプトロンを見つけると言うものです。勾配の大きい箇所は推論結果の計算に大きく寄与した箇所なので、結果的に何を重要と考え、結果を出力できたかがわかるだろうという考え方です。
JetRacerやJetBotの転移学習に用いられるモデルはResNet-18で、PyTorchを利用して実装されています。ResNet-18でPyTorchを利用して可視化を行います。GradCAM自体の実装は以下のブログの内容を利用しました。こちらのブログではResNet-32を利用されていますが、Grad-CAMに渡すレイヤー名などは変わらないので、そのまま利用可能です。
モデルをGradCAMクラスでラップし、fowardを実行後の処理を変えます。参考にしているブログでは画像が出力となっていますが、AI RC Carの場合は2つの変数が出力になります。JetRacerの場合は出力のうち、推論された画像のx座標の値にのみ着目すればいいため、以下のように実行します。
# sample_image[None, ...]はモデルとロードした画像に合わせて変更、こちらはJetBotのサンプルでの実行方法 model_output = grad_cam.forward(sample_image[None, ...]) grad_cam.backward_on_target(model_output[0][1])
実際の可視化画像
実際に走行時の画像をGradCamで可視化した映像が以下になります。注意したいのは以下は実際にはJetBotで撮影した連続画像をオフラインで処理しています。また、このデータの場合は画像上の座標位置ではなく、ステアリング角を学習した場合のものです。ですが、JetRacerでも同様の方法で可視化が可能です。
Visualize Autopilot with Grad-CAM
この動画ではステアリング変更に、コースの白線が特徴として利用されていることがわかります。また、大きくカーブするシーンでは周囲の環境も特徴としてよく利用されているようです。JetRacerで画像座標を推論する場合もほぼ同じ結果が出ると考えています。
まとめ
GardCAMでの可視化は、直感的に走行に寄与しそうな画像上の特徴にモデルが着目しているかを確認することができます。これにより、学習したモデルがコース上を正確に走れるか検討ができそうです。
AI RC Carのプロポ信号をUSBゲームパッドに変換する
この記事について
この記事はAI RC Carアドベントカレンダー13日目の記事です。今回はRC Carのプロポの入力をUSBゲームパッドとして変換する内容を紹介します。
AI RC Carとプロポ
AI RC Carでは人がプロポを使って操作した内容をJetsonNanoなどのSBC(シングルボードコンピュータ)に記録する場合が多々あります。通常、スピコンやサーボへ接続するプロポ受信機のPWM信号をなんらかの方法でSBCへ渡すことで実現します。一般的なラジコンのPWM信号は1msから2msのパルス幅となっています。これをSBCで直接読み込む方法はありません。PWMの値を読む前にOSの割り込みなり入るため、専用ハードを用意する必要があります。RTOSなどを利用すればできるかもしれませんが、基本的にはSBCで実施するのは厳し目です。
マイコンを利用したPWM信号のデコード
こういった用途に非常に適しているのがマイコンです。一般的に多くのマイコンではこういったPWM信号のパルス幅を計測するための機能が用意されています。Arduinoなどにもようしてありますが、今回は格安マイコンのBluepill(STM32F103)を利用します。理由としては以前購入してそのまま放置していた。Amazonで格安で購入できる点です。
BluepillとSBCの接続
Bluepillはmicro USBを搭載しており、HIDにも対応しています。HIDはいわゆる、マウスやキーボード、ゲームパッドなどのインタフェースとしてホストPCへ接続できる機能です。今回はBuluepillをゲームパッドとしてSBCへ接続します。ゲームパッドジョイスティックとして認識されるためプログラムから制御可能です。Pythonであれば、PyGamesなどを使って制御できます。また、HTML5のGamePad APIも利用できます。簡単に試すにはPCへUSBで接続、以下のサイトを最新のChromeブラウザから開くとGamePadとして認識していることがわかります。
プログラム
Bluepillの詳しい開発方法は掲載していません。Arduino IDEで開発できるように環境を整えてください。実際に書き込みを行ったソースコードは以下のようになっています。HIDのライブラリであるUSBCompositeをあらかじめインストールしておきます。PB5とPB6にプロポの受信機のCH1, CH2をそれぞれ接続します。USBでホストに接続するとXBOX360のコントローラとして認識され、CH1が左ジョイスティックの縦操作、CH2が右ジョイスティックの横操作に対応します。
#include <USBComposite.h> #define CHANNEL_NUM 2 #define DEBUG 1 const int WIDTH = 410; byte PWM_PIN[CHANNEL_NUM] = {PB5,PB6}; int base[CHANNEL_NUM] = {0,0}; int value[CHANNEL_NUM] = {0,0}; float joystick[CHANNEL_NUM] = {0,0}; USBXBox360 XBox360; void setup() { // put your setup code here, to run once: pinMode(PC13, OUTPUT); digitalWrite(PC13, HIGH); pinMode(PWM_PIN[0], INPUT); pinMode(PWM_PIN[1], INPUT); delay(100); base[0] = pulseIn(PWM_PIN[0], HIGH, 29412); base[1] = pulseIn(PWM_PIN[1], HIGH, 29412); XBox360.begin(); digitalWrite(PC13, LOW); delay(1000); } void loop() { for(int i=0; i < CHANNEL_NUM; i++){ value[i] = pulseIn(PWM_PIN[i], HIGH, 29412); joystick[i] = map(value[i], long(base[i] - WIDTH), long(base[i] + WIDTH), -30000, 30000); } XBox360.X(joystick[0]); XBox360.XRight(joystick[1]); }
init内では入出力ピンの初期化処理とHIDデバイスの初期化を行っています。メインループの中ですが、逐次PWMピンの値を読み取り、-30000から30000の間に値をマッピングし直しているのみです。その後はXBox360のコントローラに対応した値をセットしてホスト側PCに入力を送っています。
まとめ
ラジコンプロポの信号をRaspberryPiやJetson Nanoで読み取る方法は以外にめんどくさく情報がありません。Arduinoで一度受け取ってからHIDで送る方法は大変お手軽で思ったよりも簡単に作れるので、AI RC Car作成の際に一つ用意しておくと良いでしょう。
AWS RoboMakerのJetbotチュートリアルをやってみる(シミュレーター編)
この記事について
この記事はAI RC Car アドベントカレンダー11日目の記事です。今日はAWS RoboMakerのJetbotチュートリアルをやってみます。このチュートリアルは2019年12月に開催された AWS re:Invent 2019でワークショップとして開催された内容です。実際のワークショップではAWSで用意されたアカウントを使いチュートリアルを進めていたようです。個人のアカウントで進めることもできましたが、実施には一手間必要だったのでその部分について触れていきます。AI RC Carと言いつつも、全然RCじゃないですし、むしろロボットっぽいですが、この分野はいずれ融合します。多分。。。
AWS RoboMakerとは
AWS RoboMakerはAWSのサービスの一つです。クラウド上でROSアプリケーションの開発とシミュレーションができます。ROSアプリケーションの開発はCloud9と呼ばれるWEBブラウザで動くIDEで開発を行います。AWS のコグニティブサービスやKinesisによるビデオストリーミングをサポートしています。また、開発後はGazeboでのシミュレーションをWEBブラウザ上で確認可能です。さらに、AWS Greengrass IoTと連携したロボットのフリート管理機能ではロボットへアプリケーションを直接デプロイすることが可能です。
AWS RoboMakerとJetBot
AWS RoboMakerのチュートリアルではJetBotが題材となっています。チュートリアルのマテリアルにはJetBotのROSアプリケーションのサンプルとGazeboシミュレータで利用可能なJetBotの3Dモデルが含まれています。もちろん開発した内容は実機のJetBotへインターネット越しにデプロイできます。JetBotでROS開発を行う場合は強力なツールになりえます。
AWS RoboMakerのチュートリアル
対象とするAWS RoboMakerのチュートリアルはJetBot Teleop
のチュートリアルです。
このチュートリアルは3つの構成に分かれてます。一つはWorkshop setup
でチュートリアルを進めるのに必要なリソースをAWS Cloud Formationを使い自動構築します。その次はActivity #1: An Introduction to ROS Development
で、AWS RoboMaker上でのROSアプリケーションのビルドとシミュレーションの実行を学習します。最後はActivity #2: Deploying ROS Applications
です。実機へのROSアプリケーションのデプロイ方法を学習します。この記事では2つ目までを対象とします。また、チュートリアルの中ではROSのプログラミングやアプリケーションの基本構成については深く語られていませんので、そのあたりの知識は別途学習する必要があります。
AWS RoboMaker環境の構築
まずはチュートリアルのWorkshop setupを進めていきます。このチュートリアルではAWSがイベント向けに用意した環境を利用するか、自分のAWS環境で進めるかで手順が異なります。今回は自分のAWS環境を利用するので、No. I will use own AWS account.
を選択します。
下の方に進んでいくとLaunch stack
ボタンを押下すると、AWS CloudFormationの画面がブラウザ上に表示されます。AWSコンソールにログインしていない場合は認証画面が開くので、ログインしましょう。
右上でリージョンがオレゴンになっているので、東京に切り替えます。この後のCloud9IDEを立ち上げる際に、オレゴンだとm4.largeのインスタンスが利用できない旨エラーが出ました。
デザイナーでCloudFormationで作成されるリソースを確認しておきましょう。VPCと2つのサブネット、S3バケットとAWS Robomaker用のロールが2つ作成されます。確認したら右上の「閉じる」を押して下の画面に戻り、右下の「次へ」を押下します。
画面が切り替わったら「スタックの名前」と「S3BucketName」に名前を入れます。いずれも任意の名前を入れれば大丈夫ですが。S3BucketNameだけはグローバルに一意な名前をつける必要があります。「次へ」を押して、次の画面に進みます。次の画面の下の方に、AWS Roleを自動で作成する旨の警告が表示されていますので、チェックボックを入れて確認しましょう。そのまま「作成」を押せばリソースの自動作成が始まります。しばらくすると作成が完了します。
RoboMakerによる開発
続いて、Activity #1: An Introduction to ROS Development
です。まずは手順にしたがい、AWS RoboMakerを使って開発環境を立ち上げます。この時、選択するVPCはdefaultと表示されているものと、先ほどのCloudFormationで作成されたものの2つが表示されます。一旦私はCloudFormationで作成されたVPCを選択しました。subnetは適当に。
その後、CloudFormationでmod-xxxxx
ぽい名前を控えるとありますが、実際には環境構築で作成したS3が含まれるCloudFormationでつけた環境の名前で大丈夫です。Cloud9立ち上げ時にもう一つCloudFormationの環境が作られますが、そちらではありません。引き続き、手順にしたがってGitからサンプルコードをダウンロードとinstall_deps.sh
の実行まで進めます。
この後、ビルドを実行する手順になりますが、すぐに実行すると他のaptのプロセスがロックした状態で正常にビルドできません。しばらく待って実行するが、psコマンドでaptのプロセス番号を特定し、killコマンドで停止させるなどで回避可能です。
その後のsimulationの実行では実行時に権限がないと言われ、実行ができません。設定からIAMRoleを追加します。Add or Edit Configurations
を選択して設定画面を開きます。
設定画面で、SIMULATION=>JetBotCircle Simulation を選択して、IAM roleでrobomaker-simulation-role
を選択します。同様にteleopの方にも設定しておきましょう。
ここまでくれば後は手順通りに進めます。シミュレーション実行後、Gazeboの画面を開くと以下のようにJetBotが回転している状態が表示されます。Teleopの方もクライアントをダウンロードして、認証情報を設定すればローカルマシンからシミュレーション上のJetBotを操作することが可能です。
まとめ
JetBotの開発にROSを利用しようとするとAWS RoboMakerは選択肢になりえそうです。開発からシミュレーションまで環境が最初から準備されているのがマネージドサービスの強みですね。一方でちょっと遊んだだけで2ドルくらい取られるので、本当に使い始めたらいくらかかるのかちょっと心配です。また、AWSとROS両方の知識を求められるので、学習コストは非常に高い気がします。もし、ROS(Gazeboを)動かせるPCを運良く持っていたのであれば、大人くしROSの環境構築をしてしまった方が良いかもしれません。実機へのデプロイができたら続きを書きます。
JetsonNanoのカメラの色味を補正する。
この記事について
この記事はAI RC Car アドベントカレンダー6日目の記事です。この記事ではAI RC Carによく用いられるJetsonNanoに接続されるカメラの設定について自分への備忘録の意味を込めて紹介します。今回の情報はFacebookのコミュニティ「AIでRCカーを走らせよう!」でinachiさんから提供された情報をベースに記載しています。
SainSmart IMX219
AI RC Carではコースを広く写すことはもとより、推論時の手がかりとなるように周囲を広く撮影できるカメラが利用されます。よく利用されるが書くとしてFPV 160度の広角カメラが用いられることが多いように感じます。Jetson Nanoの場合はIMX 219 FPV160というカメラが利用可能です。Amazonでは3500円程度で販売されています。
こちらのカメラをJetsonNanoに接続すれば、OpenCVを通してすぐにカメラ画像にアクセスすることが可能です。しかしこのカメラを通して得られる画像は以下のように周囲が赤みがかった画像が表示されます。
カメラパラメータの調整
実はこのカメラパラメータの調整は簡単に調整することが可能です。次のブログに記載があります。
Waveshareのサイトで公開されているパメラパラメータのチューニングファイルをダウンロードしてカメラのセッティングフォルダに配置するだけです。
wget https://www.waveshare.com/w/upload/e/eb/Camera_overrides.tar.gz tar zxvf Camera_overrides.tar.gz
sudo cp camera_overrides.isp /var/nvidia/nvcam/settings/ sudo chmod 664 /var/nvidia/nvcam/settings/camera_overrides.isp sudo chown root:root /var/nvidia/nvcam/settings/camera_overrides.isp
念の為、camera_overrides.isp
の中身をのぞいてみると、NVIDIAが公開したファイルだということがわかります。色相の設定やホワイトバランス、カラーなどの設定が記載されていますが、詳細はわかりません。
まとめ
ディープラーニングを利用した学習走行にカメラの色味がどこまで影響があるかわかりませんが、OpenCVを使った前処理などを行う場合は必ず影響を受けます。設定ファイルを配置するだけでクリアな画像を撮影できるため、当該の事象が出る方は是非設定したほうが良いでしょう。
JetBotのraod_followingサンプル学習時のGPUメモリ解放忘れについて
この記事について
この記事はAI RC Carアドベントカレンダー4日めの記事です。4日目の今日は小ネタ中の小ネタです。JetBotのサンプルに含まれるroad_followingの学習ノートブックの修正についてです。
road_followingは学習が遅い
JetBotのサンプルにはroad_followingと呼ばれる、コースを追従するためのサンプルがあります。モデルを学習するためのtrain_model.ipynb
ノートブックで学習を行おうとすると学習に時間がかかり、また500枚程度の画像で、GPUメモリが溢れてしまい学習ができなくなってしまいます。
loss値の解放忘れ
この現象の原因は学習時に各エポックごとのloss値の総和を求める際にlossの計算結果をGPUメモリに乗せたまま計算していることが原因と考えられます。
train_model.ipynb
の最終ブロックをみてみると以下のようなコードをみることができます。
for images, labels in iter(train_loader): images = images.to(device) labels = labels.to(device) optimizer.zero_grad() outputs = model(images) loss = F.mse_loss(outputs, labels) train_loss += loss loss.backward() optimizer.step() train_loss /= len(train_loader)
この時、mse_lossの計算結果はGPUメモリ上に乗ったままになっています。ですので、上記コードの7行目をtrain_loss += float(loss)
とします。これはfloatでキャストしているだけですが、Pytorchのloss.detach().cpu()と同じ効果を得られるようです。学習時とバリデーション時両方忘れずに修正しましょう。
この修正を夏頃プルリクエストで投げてみましたが、あまりにもニッチすぎるのか、それとも外部からのプルリクエストは受け付けない方針なのか、マージされません。ですので個別に修正することをお勧めします。
## まとめ
とても微妙な箇所ですが、実際にroad_followingの学習を初めてみると他のサンプルの学習に比べてすこい遅い感じがするので、road_followingを試す場合はこの修正をお勧めします。
JetRacerの速度制御について考えてみた。
この記事について
この記事はAI RC Car アドベントカレンダー 2019 3日目の記事です。3日目の記事はJetRacerの速度制御についてNVIDIAのリポジトリのブランチから考えてみたいと思います。この記事はある程度JetRacerの走行の仕組みを理解されている方向けになります。なるべく補足を入れていきますが、わかりづらい点はご容赦ください。
JetRacerの速度制御の必要性
JetRacerで利用するディープラーニングでは、撮影した画像に対して進行方向を画像上のX,Y座標として教師データを与えていきます。学習したAIモデルは入力画像のX軸方向の座標を出力します。この座標を元にステアリングの制御値を生成する。仕組みです。これだけの仕組みですが、十分にコースに追従した走行が可能となります。
以下、JetRacerリポジトリで公開されている学習の様子です。
一方で速度についてはパラメータとして与えられ、一定の速度でコース上を周回します。しかし、コースの直線部分もカーブ部分も均一な速度となるため、早すぎるとカーブではコースアウトしやすくなり、遅すぎると全体的に速度が遅くなってしまいます。そのため、JetRacerの速度をいかに最適に設定するかは周回速度を競うために重要な問題となります。
[速度の図]
JetRacerの速度制御方法
速度制御の方法としてもっとも簡単に思いつくのはステアリングの制御値に合わせて速度を加減速させる方法です。制御値の絶対値の大きさに比例して速度を遅くすれば直線部分とカーブ部分で速度のメリハリをつけることができます。全体的に速度を上げて走行させることができるようになります。
しかし、この方法には課題があります。まず、パラメータをヒューリスティックに決めなければいけません。走行させる速度の段階や、ステアリングの値に合わせてどういったルールで速度を落としていくのかを調整しなければいけません。ある程度数式で近似する方法もあれば、IF-THENのルールに落とし込む方法もあるでしょう。何れにしても走行のたびに試行錯誤が必要です。 また、速度の調整が遅れてしまうという問題があります。ステアリングの値が大きくなる時点で車体はすでにカーブに入っています。この時点で速度を落としても車体の慣性により十分に速度を落とすことはできません。実際の車の運転でもカーブ前に十分に速度を落として、。カーブを抜けるとともに速度を上げていく操作をします。ルールベースの方法ではこういったアクセル操作を行うことはできません。
コースの状態を把握しながら速度を加減速するためには、現在撮影している画像からその後のステアリングの状態、先のカーブの状態を推定する必要があります。しかし、現在のコースの状態のみからそれを把握するのは難しくなります。このアクセル操作を実現するためにはいかに、コースの形状を学習させていくかが鍵となりそうです。
circuit_learningに見る速度制御へのディープラーニングの適用
NVIDIAのJetRacerリポジトリにはcircuit_learningと呼ばれるブランチが存在します。これはJetRacerの速度制御をディープラーニングで実現させるサンプルノートブックが含まれています。このブランチのnotebook/circuit_learningフォルダ内がそれです。
GitHub - NVIDIA-AI-IOT/jetracer at circuit_learning
このcircuit_learningは前述のコースの形状を学習させるために工夫が施されています。まず、あらかじめ学習させたステアリングのみを制御するモデルを使いコースを走行します。走行時に、推論したステアリング値をラベルとしてコースの画像を収集します。収集された画像は時系列順に連番が振られています。これが速度制御モデルの学習データとなります。
circuit_learningの学習ではこの学習データを学習しますが、ラベルデータの扱いを工夫しています。N番目の画像のラベルはN+TIMESTEPまでの画像のラベルに係数をかけて足し合わせたものです。この時、係数は指数的に値が小さくなる関数を設定します。これは画像の枚数が増えるにつれて、値が小さくなるようにする工夫です。ここの画像のラベルは前述の通り、ステアリング値です。先のステアリング値を見ていくことで、将来、コースがどう変化するかを見ています。さらに指数的に変化する係数をかけることで、よりコースの近い状態が影響が大きくなるようにしているようです。つまり、カーブが近づくにつれて値が大きくなるラベルを設定することができます。実際のコードはtrain_model.pyのCircuitDataset
クラスで実現しています。
コード上からラベルづけに関係する部分を抜粋すると以下のようになります。
# 重みの作成、num_timestepsは先読みする枚数が設定されている。 gain = torch.exp(-3e-2*torch.linspace(0.0, num_timesteps, num_timesteps)) # ラベルをつける画像からnum_timesteps枚分のラベルを取得する。 for i in range(self.num_timesteps): path = os.path.splitext(os.path.basename(self.image_paths[idx + i]))[0] x = float(int(path.split('_')[1]) - 50) / 50.0 target[i] = x # 重みをかけて正規化し、総和を取る label = torch.sum(self.gain * torch.abs(target) / torch.sum(self.gain), dim=0, keepdim=True)
上記を学習させると、カーブが近づくにつれて出力される値は「大きく」なります。そのため、実際に速度制御に使う場合は逆数を取るなど工夫が必要です。サンプルではlive.demo.ipynbで速度をcar.throttle = np.exp(-15*out) * speed_gain.value + speed_offset.value
と計算しています。ここでoutは推論結果の出力です。
まとめ
この方法で速度制御をしっかりできるようになりましたと記載できればかっこいいのですが、今現在これで本当に速度が制御できるかわかっていません。何度かテストデータに対して推論を行い、速度変化をシミュレートしてましたが、思うような結果が出ていません。また、計算に使う画像の枚数や、ラベル計算式のハイパーパラメーターなど依然としてヒューリスティックな部分が残っているのも気になります。大きな方針としては良いのでしょうがまだまだ改良が必要です。引き続き、色々試してみたいと思います。