【第2章】2-7_SSD_training.ipynb内のでのエラー
Opened this issue · 7 comments
小川様
私は大学の研究の一環で、SSDを用いて数式検出を行おうとしています。その際に、小川様のテキストを参考にさせていただいており、誠にありがとうございます。
研究においては独自でアノテーションされたデータを用意し、テキストを参考にさせていただいてSSDを実装していたのですが、2-7_SSD_training.ipynb内の最後のブロックでtrain_model()を実行する際に下記のようなエラーが出てしまいます。
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_18292/1042517758.py in <module>
1 # 学習・検証を実行する
2 num_epochs= 11
----> 3 train_model(net, dataloaders_dict, criterion, optimizer, num_epochs=num_epochs)
~\AppData\Local\Temp/ipykernel_18292/3990735102.py in train_model(net, dataloaders_dict, criterion, optimizer, num_epochs)
46
47 # データローダーからminibatchずつ取り出すループ
---> 48 for images, targets in dataloaders_dict[phase]:
49 print("1")
50 # GPUが使えるならGPUにデータを送る
~\AppData\Local\Programs\Python\Python39\lib\site-packages\torch\utils\data\dataloader.py in __next__(self)
519 if self._sampler_iter is None:
520 self._reset()
--> 521 data = self._next_data()
522 self._num_yielded += 1
523 if self._dataset_kind == _DatasetKind.Iterable and \
~\AppData\Local\Programs\Python\Python39\lib\site-packages\torch\utils\data\dataloader.py in _next_data(self)
559 def _next_data(self):
560 index = self._next_index() # may raise StopIteration
--> 561 data = self._dataset_fetcher.fetch(index) # may raise StopIteration
562 if self._pin_memory:
563 data = _utils.pin_memory.pin_memory(data)
~\AppData\Local\Programs\Python\Python39\lib\site-packages\torch\utils\data\_utils\fetch.py in fetch(self, possibly_batched_index)
47 def fetch(self, possibly_batched_index):
48 if self.auto_collation:
---> 49 data = [self.dataset[idx] for idx in possibly_batched_index]
50 else:
51 data = self.dataset[possibly_batched_index]
~\AppData\Local\Programs\Python\Python39\lib\site-packages\torch\utils\data\_utils\fetch.py in <listcomp>(.0)
47 def fetch(self, possibly_batched_index):
48 if self.auto_collation:
---> 49 data = [self.dataset[idx] for idx in possibly_batched_index]
50 else:
51 data = self.dataset[possibly_batched_index]
c:\Users\kwmrs\Documents\studies\SSD_IBEM\utils\ssd_model.py in __getitem__(self, index)
213 前処理をした画像のテンソル形式のデータとアノテーションを取得
214 '''
--> 215 im, gt, h, w = self.pull_item(index)
216 return im, gt
217
c:\Users\kwmrs\Documents\studies\SSD_IBEM\utils\ssd_model.py in pull_item(self, index)
229 print("anno_list =")
230 print(anno_list)
--> 231
232 # 3. 前処理を実施
233 img, boxes, labels = self.transform(
c:\Users\kwmrs\Documents\studies\SSD_IBEM\utils\ssd_model.py in __call__(self, img, phase, boxes, labels)
177 前処理のモードを指定。
178 """
--> 179 return self.data_transform[phase](img, boxes, labels)
180
181
c:\Users\kwmrs\Documents\studies\SSD_IBEM\utils\data_augumentation.py in __call__(self, img, boxes, labels)
58 def __call__(self, img, boxes=None, labels=None):
59 for t in self.transforms:
---> 60 img, boxes, labels = t(img, boxes, labels)
61 return img, boxes, labels
62
c:\Users\kwmrs\Documents\studies\SSD_IBEM\utils\data_augumentation.py in __call__(self, image, boxes, labels)
272
273 # calculate IoU (jaccard overlap) b/t the cropped and gt boxes
--> 274 overlap = jaccard_numpy(boxes, rect)
275
276 # is min and max overlap constraint satisfied? if not try again
c:\Users\kwmrs\Documents\studies\SSD_IBEM\utils\data_augumentation.py in jaccard_numpy(box_a, box_b)
33 jaccard overlap: Shape: [box_a.shape[0], box_a.shape[1]]
34 """
---> 35 inter = intersect(box_a, box_b)
36 area_a = ((box_a[:, 2]-box_a[:, 0]) *
37 (box_a[:, 3]-box_a[:, 1])) # [A,B]
c:\Users\kwmrs\Documents\studies\SSD_IBEM\utils\data_augumentation.py in intersect(box_a, box_b)
16
17 def intersect(box_a, box_b):
---> 18 max_xy = np.minimum(box_a[:, 2:], box_b[2:])
19 min_xy = np.maximum(box_a[:, :2], box_b[:2])
20 inter = np.clip((max_xy - min_xy), a_min=0, a_max=np.inf)
ValueError: operands could not be broadcast together with shapes (4,3) (2,)
まず、テキスト通りのコードで、テキスト通りのデータを用いて学習を行った場合には問題なく実行できます。
私の用いるデータは、2クラス(埋め込み数式とディスプレイ数式)を検出しようとしています。アノテーションの保存形式が独自のtxtファイルとなっており、それに合わせてssd_model.py内のmake_datapath_list()やAnno_xml2list()を大きく変更いたしました。また、検出するクラス数が2クラスであることから、(元のクラス数+背景クラスの1)である21を記述されている部分を3に変更するようにしました。これらの変更はまず、2-2-3_Dataset_DataLoader.ipynb、2-4-5_SSD_model_forward.ipynb、2-6_loss_function.ipynbでそれぞれ実行して試しており、その際にはエラーは出ずに実行できました。
issue #190 の方の質問内容と似通っているところがあって参考にもさせていただき、どこかしらでlistやarray?の形が間違っているせいで出てしまっているエラーなのかと愚考します。しかし、これ以上の原因究明方法が分からず、手詰まりになってしまいました。
お忙しい中大変恐縮ですが、ご協力いただけますと幸いです。
ご質問ありがとうございます。
独自に取り組んでいただき、著者としてもとても嬉しいです。
ご自身で可能性として挙げられている通り、私も、
どこかしらでlistやarray?の形が間違っているせいで出てしまっているエラーなのか
と思います。
utils\data_augumentation.pyの
17 def intersect(box_a, box_b):
---> 18 max_xy = np.minimum(box_a[:, 2:], box_b[2:])
でエラーが生まれており、
operands could not be broadcast together with shapes (4,3) (2,)
のエラーメッセージとなっています。
まずは、本書の通りに動かした場合のbox_aのshapeと、現在のご自身のプログラムのbox_aのshapeを確認し、
同じになっているか、それぞれの値をprintして調べるのが良いと思います。
もし、違うshapeになっていれば、その後、コードを辿って、どのテンソルからshapeが変わっているのか、
愚直にひとつずつprintしてshapeを確認していくのをおすすめします(ちょっと、面倒で大変ですが確実です)。
どうぞよろしくお願い致します。
すぐにご返信を頂いていたにもかかわらず、ご報告が遅くなってしまって申し訳ありません。
質問させていただいた内容についてなのですが、いろいろ試行錯誤してみたものの正しく解決することができず、以下のようにDataTransformクラスの一部をコメントアウトすることで場当たり的な対処をすることになってしまいました。下記の修正によって一応学習はできたのですが、自身が取り組んでいるタスクである数式検出においては望む結果は得られませんでした。うまくいかない原因として以下のようなことを想定しています。
- 文書画像全体をそのまま300×300にリサイズしてしまっていることで、文字や数式としての細部がつぶれてしまっている
- SSDではバウンディングボックスの出力として、数種類の大きさとアスペクト比を持ったデフォルトボックスを用意(8732個)し、その各デフォルトボックスに対して大きさを調整したものと各クラスの信頼度を得る。しかし、実装したコードのデフォルトボックスのアスペクト比は最も横長なものでも1:3である一方で、数式にはより横長なものも存在する
そこで、さらにモデルを修正して改善を図ろうと考えているのですが、以下のようなことはコードを一部修正することで可能でしょうか。
- 入力画像を圧縮するのではなく、切り取ることによって300×300の入力とする。
- デフォルトボックスのアスペクト比を増やす、もしくは変更する。
コードを書く力が不足しており、これらを自力で実装するのに手間取っております。コードのどのあたりを修正すれば実現可能であるのか、アドバイスを頂ければ幸いです。このような場で質問させていただくような内容ではないことは重々承知なのですが、もしよろしければよろしくお願いいたします。
class DataTransform():
"""
画像とアノテーションの前処理クラス。訓練と推論で異なる動作をする。
画像のサイズを300x300にする。
学習時はデータオーギュメンテーションする。
Attributes
----------
input_size : int
リサイズ先の画像の大きさ。
color_mean : (B, G, R)
各色チャネルの平均値。
"""
def __init__(self, input_size, color_mean):
self.data_transform = {
'train': Compose([
ConvertFromInts(), # intをfloat32に変換
ToAbsoluteCoords(), # アノテーションデータの規格化を戻す
#PhotometricDistort(), # 画像の色調などをランダムに変化
#Expand(color_mean), # 画像のキャンバスを広げる
RandomSampleCrop(), # 画像内の部分をランダムに抜き出す
#RandomMirror(), # 画像を反転させる
ToPercentCoords(), # アノテーションデータを0-1に規格化
Resize(input_size), # 画像サイズをinput_size×input_sizeに変形
SubtractMeans(color_mean) # BGRの色の平均値を引き算
]),
'val': Compose([
ConvertFromInts(), # intをfloatに変換
Resize(input_size), # 画像サイズをinput_size×input_sizeに変形
SubtractMeans(color_mean) # BGRの色の平均値を引き算
])
}
def __call__(self, img, phase, boxes, labels):
"""
Parameters
----------
phase : 'train' or 'val'
前処理のモードを指定。
"""
return self.data_transform[phase](img, boxes, labels)
ご返信ありがとうございます。
上記のコメントアウトで動作させられたとのこと、重要な情報をありがとうございます。
入力画像を圧縮するのではなく、切り取ることによって300×300の入力とする。
こちらは、torchvisionの前処理のなかでも、RandomCropを使用することで可能です
https://pytorch.org/vision/main/generated/torchvision.transforms.RandomCrop.html
その他前処理は以下が参考になります
https://pytorch.org/vision/stable/transforms.html
デフォルトボックスのアスペクト比を増やす、もしくは変更する。
こちらはSSDのコードを変更すれば可能ではありますが、少し大変です。
アスペクト比を決めている部分を変更するか、新たなアスペクト比を追加するかです。
該当してくる部分がこことことで、こうしてくださいと回答するのは、
Issueでは対応できるレベルを超えてしまい大変なので、大変申し訳ございません。
(2-7_SSD_training.ipynbの、
# SSD300の設定
ssd_cfg = {
'num_classes': 21, # 背景クラスを含めた合計クラス数
'input_size': 300, # 画像の入力サイズ
'bbox_aspect_num': [4, 6, 6, 6, 4, 4], # 出力するDBoxのアスペクト比の種類
'feature_maps': [38, 19, 10, 5, 3, 1], # 各sourceの画像サイズ
'steps': [8, 16, 32, 64, 100, 300], # DBOXの大きさを決める
'min_sizes': [30, 60, 111, 162, 213, 264], # DBOXの大きさを決める
'max_sizes': [60, 111, 162, 213, 264, 315], # DBOXの大きさを決める
'aspect_ratios': [[2], [2, 3], [2, 3], [2, 3], [2], [2]],
}
部分を適切に変更すればいけるのでは?と考えています。ただ絶対にこれでいけるかは自信がなく。
)
ぜひコードを読みながら、挑戦いただければ幸いです。
いつもお忙しい中大変丁寧にご回答いただきまして本当にありがとうございます。現状試せていることについてご報告させていただきます。
デフォルトボックスのアスペクト比を増やす、もしくは変更する。
ちょうどご指摘いただいている個所と、ssd_model.pyのDBoxクラスをわずかに修正することで実現が可能でした。私の場合はsource1~3を従来の6種類+アスペクト比{5, 7, 10}のものを加えた9種類に変更してみました。まだ望む結果を得る段階には至っていないのですが、比較的横長のものが増えた気がします。変更後のコードを以下に記載させていただきます。
ssd_model.py
class DBox(object):
def __init__(self, cfg):
super(DBox, self).__init__()
# 初期設定
self.image_size = cfg['input_size'] # 画像サイズの300
# [38, 19, …] 各sourceの特徴量マップのサイズ
self.feature_maps = cfg['feature_maps']
self.num_priors = len(cfg["feature_maps"]) # sourceの個数=6
self.steps = cfg['steps'] # [8, 16, …] DBoxのピクセルサイズ
self.min_sizes = cfg['min_sizes']
# [30, 60, …] 小さい正方形のDBoxのピクセルサイズ(正確には面積)
self.max_sizes = cfg['max_sizes']
# [60, 111, …] 大きい正方形のDBoxのピクセルサイズ(正確には面積)
self.aspect_ratios = cfg['aspect_ratios'] # 長方形のDBoxのアスペクト比
def make_dbox_list(self):
'''DBoxを作成する'''
mean = []
# 'feature_maps': [38, 19, 10, 5, 3, 1]
for k, f in enumerate(self.feature_maps):
for i, j in product(range(f), repeat=2): # fまでの数で2ペアの組み合わせを作る f_P_2 個
# 特徴量の画像サイズ
# 300 / 'steps': [8, 16, 32, 64, 100, 300],
f_k = self.image_size / self.steps[k]
# DBoxの中心座標 x,y ただし、0~1で規格化している
cx = (j + 0.5) / f_k
cy = (i + 0.5) / f_k
# アスペクト比1の小さいDBox [cx,cy, width, height]
# 'min_sizes': [30, 60, 111, 162, 213, 264]
s_k = self.min_sizes[k]/self.image_size
mean += [cx, cy, s_k, s_k]
# アスペクト比1の大きいDBox [cx,cy, width, height]
# 'max_sizes': [60, 111, 162, 213, 264, 315],
s_k_prime = sqrt(s_k * (self.max_sizes[k]/self.image_size))
mean += [cx, cy, s_k_prime, s_k_prime]
# その他のアスペクト比のdefBox [cx,cy, width, height]
for ar in self.aspect_ratios[k]:
mean += [cx, cy, s_k*sqrt(ar), s_k/sqrt(ar)]
mean += [cx, cy, s_k/sqrt(ar), s_k*sqrt(ar)]
##############
#####追加#####
##############
# 横長のdefBox [cx,cy, width, height]
if k<3:
aspect_ratio = [5, 7, 10]
for ar in aspect_ratio:
mean += [cx, cy, s_k*sqrt(ar), s_k/sqrt(ar)]
# DBoxをテンソルに変換 torch.Size([8732, 4])
output = torch.Tensor(mean).view(-1, 4)
# DBoxが画像の外にはみ出るのを防ぐため、大きさを最小0、最大1にする
output.clamp_(max=1, min=0)
return output
2-7_SSD_training.ipynb
# SSD300の設定
ssd_cfg = {
'num_classes': 3, # 背景クラスを含めた合計クラス数
'input_size': 300, # 画像の入力サイズ
'bbox_aspect_num': [9, 9, 9, 6, 4, 4], # 出力するDBoxのアスペクト比の種類 #####変更#####
'feature_maps': [38, 19, 10, 5, 3, 1], # 各sourceの画像サイズ
'steps': [8, 16, 32, 64, 100, 300], # DBOXの大きさを決める
'min_sizes': [30, 60, 111, 162, 213, 264], # DBOXの大きさを決める
'max_sizes': [60, 111, 162, 213, 264, 315], # DBOXの大きさを決める
'aspect_ratios': [[2, 3], [2, 3], [2, 3], [2, 3], [2], [2]], #####変更#####
}
入力画像を圧縮するのではなく、切り取ることによって300×300の入力とする。
こちらはまだ試せていないので、いただいたアドバイスを参考に取り組んでみたいと思います。
現状の問題
学習した重みを利用していくつかの画像で検出を行ったところ、20epoch目までの重みを用いるとどの画像に対しても同じバウンディングボックスが示されてしまい、30epoch目以降の重みを用いるとバウンディングボックスが一切検出されなくなってしまいます。このような現象が起こる一般的な原因で、もしなにか思い当たることがございましたらご指摘いただければ幸いです。これはそもそも学習の段階で何か間違っている可能性があるのでしょうか。それともタスクの難しさやデータセットの悪さによるものなのでしょうか。
いろいろと独自に試していただき、そしてその知見を共有いただけて、非常に感謝いたします。
学習した重みを利用していくつかの画像で検出を行ったところ、20epoch目までの重みを用いるとどの画像に対しても同じバウンディングボックスが示されてしまい、30epoch目以降の重みを用いるとバウンディングボックスが一切検出されなくなってしまいます。このような現象が起こる一般的な原因で、もしなにか思い当たることがございましたらご指摘いただければ幸いです。
アドバイスとしては、こうした挙動の確認も重要ですが、epochごとのlossの推移も気になるところです。
同様に、SSDを独自に試された方も、lossがnanに飛ぶ状況になり、その後解決されていました。
それらの経緯は、こちらのIssueの
の、#190 (comment)
あたりからになります
参考になれば、幸いです。
引き続きのアドバイスをありがとうございます。いろいろと改良を行っていった結果、
入力画像を圧縮するのではなく、切り取ることによって300×300の入力とする。
という部分についてはdata_augumentation.pyで定義されたRandamSmapleCropクラスを改造したSampleCropクラスを作成することにより実現できました。切り取るサイズが300×300だと小さすぎたので、600×600を切り取って300×300にリサイズするようにしました(リサイズはまた別のクラスでの処理)。少なくとも一つは正解boxが含まれるように、一定回数ランダムで切り取る試行を行い、条件を満たさなければ一つ目のboxが強制的に含まれるようにしました。念のため、以下にその内容を記載させていただきます。
class SampleCrop in data_augumentation.py
class SampleCrop(object):
"""Crop
Arguments:
img (Image): the image being input during training
boxes (Tensor): the original bounding boxes in pt form
labels (Tensor): the class labels for each bbox
mode (float tuple): the min and max jaccard overlaps
Return:
(img, boxes, classes)
img (Image): the cropped image
boxes (Tensor): the adjusted bounding boxes in pt form
labels (Tensor): the class labels for each bbox
"""
def __call__(self, image, boxes=None, labels=None):
height, width, _ = image.shape
w = 600
h = 600
while True:
min_iou = 0.8
max_iou = float('inf')
# max trails (50)
for _ in range(50):
current_image = image
left = random.uniform(width - w)
top = random.uniform(height - h)
# convert to integer rect x1,y1,x2,y2
rect = np.array([int(left), int(top), int(left+w), int(top+h)])
# calculate IoU (jaccard overlap) b/t the cropped and gt boxes
overlap = jaccard_numpy(boxes, rect)
# is min and max overlap constraint satisfied? if not try again
if overlap.min() < min_iou and max_iou < overlap.max():
continue
# cut the crop from the image
current_image = current_image[rect[1]:rect[3], rect[0]:rect[2],
:]
# keep overlap with gt box IF center in sampled patch
centers = (boxes[:, :2] + boxes[:, 2:]) / 2.0
#全部trueの時だけmask=1になる
# mask in all gt boxes that above and to the left of centers
m1 = (rect[0] < centers[:, 0]) * (rect[1] < centers[:, 1])
# mask in all gt boxes that under and to the right of centers
m2 = (rect[2] > centers[:, 0]) * (rect[3] > centers[:, 1])
# mask in that both m1 and m2 are true
mask = m1 * m2
# boxが大きい場合は入れる
box_sizes = boxes[:, 2:] - boxes[:, :2]
for i in range(len(mask)):
huge_box_judge = (box_sizes[i, 0] > w*2) or (box_sizes[i, 1] > h*2)
overlap_judge = overlap[i] > 0.8
if overlap_judge and huge_box_judge:
mask[i] = True
# have any valid boxes? try again if not
if not mask.any():
continue
#中心が枠内のboxだけを取得
# take only matching gt boxes
current_boxes = boxes[mask, :].copy()
# take only matching gt labels
current_labels = labels[mask]
# should we use the box left and top corner or the crop's
# 左上
current_boxes[:, :2] = np.maximum(current_boxes[:, :2], rect[:2])
# adjust to crop (by substracting crop's left,top)
current_boxes[:, :2] -= rect[:2]
# 右下
current_boxes[:, 2:] = np.minimum(current_boxes[:, 2:], rect[2:])
# adjust to crop (by substracting crop's left,top)
current_boxes[:, 2:] -= rect[:2]
return current_image, current_boxes, current_labels
# 50回を超えたらGTのboxを基準にとる
current_image = image
box_tl_x = boxes[0, 0]
box_tl_y = boxes[0, 1]
if box_tl_x <= width-w and box_tl_y <= height-w:
left = box_tl_x
top = box_tl_y
elif box_tl_x > width-w:
left = box_tl_x
top = height-w
elif box_tl_y > height-w:
left = width-w
top = box_tl_y
else:
left = width-w
top = height-w
# convert to integer rect x1,y1,x2,y2
rect = np.array([int(left), int(top), int(left+w), int(top+h)])
# calculate IoU (jaccard overlap) b/t the cropped and gt boxes
overlap = jaccard_numpy(boxes, rect)
# cut the crop from the image
current_image = current_image[rect[1]:rect[3], rect[0]:rect[2],
:]
# keep overlap with gt box IF center in sampled patch
centers = (boxes[:, :2] + boxes[:, 2:]) / 2.0
#全部trueの時だけmask=Trueになる
# mask in all gt boxes that above and to the left of centers
m1 = (rect[0] < centers[:, 0]) * (rect[1] < centers[:, 1])
# mask in all gt boxes that under and to the right of centers
m2 = (rect[2] > centers[:, 0]) * (rect[3] > centers[:, 1])
# mask in that both m1 and m2 are true
mask = m1 * m2
# boxが大きい場合は入れる
box_sizes = boxes[:, 2:] - boxes[:, :2]
for i in range(len(mask)):
huge_box_judge = (box_sizes[i, 0] > w*2) or (box_sizes[i, 1] > h*2)
overlap_judge = overlap[i] > 0.8
if overlap_judge and huge_box_judge:
mask[i] = True
# 基準にした最初のboxは入れる
mask[0] = True
#中心が枠内のboxだけを取得
# take only matching gt boxes
current_boxes = boxes[mask, :].copy()
# take only matching gt labels
current_labels = labels[mask]
# should we use the box left and top corner or the crop's
current_boxes[:, :2] = np.maximum(current_boxes[:, :2], rect[:2])
# adjust to crop (by substracting crop's left,top)
current_boxes[:, :2] -= rect[:2]
current_boxes[:, 2:] = np.minimum(current_boxes[:, 2:],
rect[2:])
# adjust to crop (by substracting crop's left,top)
current_boxes[:, 2:] -= rect[:2]
return current_image, current_boxes, current_labels
これで無事学習が進む...と思ったのですがそううまくはいかず、今度は学習の際、数epochするとlossがNaNになってしまう現象に陥りました。
アドバイスいただいた通り、epochごとのlossをloss_lとloss_cに分けて出力して確認することで原因の特定を図ったのですが、どうやら毎度loss_l、すなわちSmoothL1Lossの方で値がNaNになっているようでした。紹介した頂いたIsuueも確認し、以下のような対策を取ってみたのですが解決しませんでした。
- 学習率を下げる
1e-5あたりにしたが変わらず - バッチサイズを小さくする
データの数が少ない(訓練用で1300程度)ので、バッチサイズを4にしたが変わらず - データに誤りがないか確かめる
データの形として「アノテーションの単位が0から1の範囲で正規化した座標で、かつ、原点は左上」であることを確認。
データを取り出す作業のみを50エポック分行い、すべての画像とboxに対してNaN値が含まれないか確認したが問題なし。
クロスエントロピー誤差でNaNが出るのはよくあることなのかなと、いろいろ検索する中で感じたのですが、SmoothL1LossでNaNが出てしまう事例で参考になるものは見つけられず...。もし思い当たる原因がありましたらご助言いただけると幸いです。
何度もお時間を頂き感謝の念に堪えません。どうぞよろしくお願いいたします。
御進捗の共有を誠にありがとうございます。
おっしゃる通り、SmmothL1Lossでは計算で0除算などNaNが生まれる要因がないため、Lossに入れている、
正解のloc_tか、SSDで推定されたloc_p自体がNaNになっている可能性があります。
こちら確認してみてください。
また、バッチサイズを小さくするのは良くありません。
データ数の少なさはデータ拡張で補い、バッチサイズは変更しないままにします。
ミニバッチ内の平均的に学習することで、良いネットワークへと訓練されていくので、ミニバッチのサイズが小さいと、うまく学習できなくなります
上記のコメントより、以上の2点が気になりました。
どうぞよろしくお願いいたします。