Delete utils/torch_utils.py
Browse files- utils/torch_utils.py +0 -321
utils/torch_utils.py
DELETED
|
@@ -1,321 +0,0 @@
|
|
| 1 |
-
import datetime
|
| 2 |
-
import logging
|
| 3 |
-
import math
|
| 4 |
-
import os
|
| 5 |
-
import platform
|
| 6 |
-
import subprocess
|
| 7 |
-
import time
|
| 8 |
-
from copy import deepcopy
|
| 9 |
-
from pathlib import Path
|
| 10 |
-
|
| 11 |
-
import thop
|
| 12 |
-
import torch
|
| 13 |
-
import torch.optim as optim
|
| 14 |
-
from prefetch_generator import BackgroundGenerator
|
| 15 |
-
from torch import nn
|
| 16 |
-
from torch.utils.data import DataLoader
|
| 17 |
-
|
| 18 |
-
logger = logging.getLogger(__name__)
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def create_logger(args, setting_yaml, phase='train', rank=-1):
|
| 22 |
-
# set up logger dir
|
| 23 |
-
dataset = setting_yaml['dataset_dataset']
|
| 24 |
-
model = setting_yaml['model_name']
|
| 25 |
-
cfg_path = os.path.basename(args.log_dir).split('.')[0]
|
| 26 |
-
|
| 27 |
-
if rank in [-1, 0]:
|
| 28 |
-
time_str = time.strftime('%Y-%m-%d-%H-%M')
|
| 29 |
-
log_file = '{}_{}_{}.log'.format(cfg_path, time_str, phase)
|
| 30 |
-
# set up tensorboard_log_dir
|
| 31 |
-
tensorboard_log_dir = Path(args.log_dir) / dataset / model / (cfg_path + '_' + time_str)
|
| 32 |
-
final_output_dir = tensorboard_log_dir
|
| 33 |
-
if not tensorboard_log_dir.exists():
|
| 34 |
-
print('=> creating {}'.format(tensorboard_log_dir))
|
| 35 |
-
tensorboard_log_dir.mkdir(parents=True)
|
| 36 |
-
|
| 37 |
-
final_log_file = tensorboard_log_dir / log_file
|
| 38 |
-
head = '%(asctime)-15s %(message)s'
|
| 39 |
-
logging.basicConfig(filename=str(final_log_file),
|
| 40 |
-
format=head)
|
| 41 |
-
logger = logging.getLogger()
|
| 42 |
-
logger.setLevel(logging.INFO)
|
| 43 |
-
console = logging.StreamHandler()
|
| 44 |
-
logging.getLogger('').addHandler(console)
|
| 45 |
-
|
| 46 |
-
return logger, str(final_output_dir), str(tensorboard_log_dir)
|
| 47 |
-
else:
|
| 48 |
-
return None, None, None
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
def select_device(logger=None, device='', batch_size=None):
|
| 52 |
-
# device = 'cpu' or '0' or '0,1,2,3'
|
| 53 |
-
s = f'mtpnet 🚀 {git_describe() or date_modified()} torch {torch.__version__} ' # string
|
| 54 |
-
cpu = device.lower() == 'cpu'
|
| 55 |
-
if cpu:
|
| 56 |
-
os.environ['CUDA_VISIBLE_DEVICES'] = '-1' # force torch.cuda.is_available() = False
|
| 57 |
-
elif device: # non-cpu device requested
|
| 58 |
-
os.environ['CUDA_VISIBLE_DEVICES'] = device # set environment variable
|
| 59 |
-
assert torch.cuda.is_available(), f'CUDA unavailable, invalid device {device} requested' # check availability
|
| 60 |
-
|
| 61 |
-
cuda = not cpu and torch.cuda.is_available()
|
| 62 |
-
if cuda:
|
| 63 |
-
n = torch.cuda.device_count()
|
| 64 |
-
if n > 1 and batch_size: # check that batch_size is compatible with device_count
|
| 65 |
-
assert batch_size % n == 0, f'batch-size {batch_size} not multiple of GPU count {n}'
|
| 66 |
-
space = ' ' * len(s)
|
| 67 |
-
for i, d in enumerate(device.split(',') if device else range(n)):
|
| 68 |
-
p = torch.cuda.get_device_properties(i)
|
| 69 |
-
s += f"{'' if i == 0 else space}CUDA:{d} ({p.name}, {p.total_memory / 1024 ** 2}MB)\n" # bytes to MB
|
| 70 |
-
else:
|
| 71 |
-
s += 'CPU\n'
|
| 72 |
-
|
| 73 |
-
if logger:
|
| 74 |
-
logger.info(s.encode().decode('ascii', 'ignore') if platform.system() == 'Windows' else s) # emoji-safe
|
| 75 |
-
return torch.device('cuda:0' if cuda else 'cpu')
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
def get_optimizer(setting_yaml, model):
|
| 79 |
-
optimizer = None
|
| 80 |
-
if setting_yaml['train_optimizer'] == 'sgd':
|
| 81 |
-
optimizer = optim.SGD(
|
| 82 |
-
filter(lambda p: p.requires_grad, model.parameters()),
|
| 83 |
-
lr=setting_yaml['train_lr0'],
|
| 84 |
-
momentum=setting_yaml['train_momentum'],
|
| 85 |
-
weight_decay=setting_yaml['train_wd'],
|
| 86 |
-
nesterov=setting_yaml['train_nesterov']
|
| 87 |
-
)
|
| 88 |
-
elif setting_yaml['train_optimizer'] == 'adam':
|
| 89 |
-
optimizer = optim.Adam(
|
| 90 |
-
filter(lambda p: p.requires_grad, model.parameters()),
|
| 91 |
-
# model.parameters(),
|
| 92 |
-
lr=setting_yaml['train_lr0'],
|
| 93 |
-
betas=(setting_yaml['train_momentum'], 0.999),
|
| 94 |
-
eps=1e-5
|
| 95 |
-
)
|
| 96 |
-
|
| 97 |
-
return optimizer
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
def save_checkpoint(epoch, name, model, optimizer, scheduler, ema, output_dir, is_best, best_fitness):
|
| 101 |
-
model_state = model.module.state_dict() if is_parallel(model) else model.state_dict()
|
| 102 |
-
checkpoint = {
|
| 103 |
-
'epoch': epoch,
|
| 104 |
-
'model': name,
|
| 105 |
-
'state_dict': model_state,
|
| 106 |
-
'ema': deepcopy(ema.ema).half(),
|
| 107 |
-
'updates': ema.updates,
|
| 108 |
-
'optimizer': optimizer.state_dict(),
|
| 109 |
-
'scheduler': scheduler.state_dict(),
|
| 110 |
-
'best_fitness': best_fitness
|
| 111 |
-
}
|
| 112 |
-
last = os.path.join(output_dir, 'last.pth')
|
| 113 |
-
last_st = os.path.join(output_dir, 'last_st.pth')
|
| 114 |
-
best = os.path.join(output_dir, 'best.pth')
|
| 115 |
-
best_st = os.path.join(output_dir, 'best_st.pth')
|
| 116 |
-
torch.save(checkpoint, last)
|
| 117 |
-
torch.save(model_state, last_st)
|
| 118 |
-
if is_best and (epoch <= 100):
|
| 119 |
-
torch.save(checkpoint, best)
|
| 120 |
-
torch.save(model_state, best_st)
|
| 121 |
-
if is_best and (epoch > 100):
|
| 122 |
-
torch.save(checkpoint, os.path.join(output_dir, 'best_{:03d}.pth'.format(epoch)))
|
| 123 |
-
torch.save(model_state, os.path.join(output_dir, 'best_st_{:03d}.pth'.format(epoch)))
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
class ModelEMA:
|
| 127 |
-
""" Model Exponential Moving Average from https://github.com/rwightman/pytorch-image-models
|
| 128 |
-
Keep a moving average of everything in the model state_dict (parameters and buffers).
|
| 129 |
-
This is intended to allow functionality like
|
| 130 |
-
https://www.tensorflow.org/api_docs/python/tf/train/ExponentialMovingAverage
|
| 131 |
-
A smoothed version of the weights is necessary for some training schemes to perform well.
|
| 132 |
-
This class is sensitive where it is initialized in the sequence of model init,
|
| 133 |
-
GPU assignment and distributed training wrappers.
|
| 134 |
-
"""
|
| 135 |
-
|
| 136 |
-
def __init__(self, model, decay=0.9999, updates=0):
|
| 137 |
-
# Create EMA
|
| 138 |
-
self.ema = deepcopy(model.module if is_parallel(model) else model).eval() # FP32 EMA
|
| 139 |
-
# if next(model.parameters()).device.type != 'cpu':
|
| 140 |
-
# self.ema.half() # FP16 EMA
|
| 141 |
-
self.updates = updates # number of EMA updates
|
| 142 |
-
self.decay = lambda x: decay * (1 - math.exp(-x / 2000)) # decay exponential ramp (to help early epochs)
|
| 143 |
-
for p in self.ema.parameters():
|
| 144 |
-
p.requires_grad_(False)
|
| 145 |
-
|
| 146 |
-
def update(self, model):
|
| 147 |
-
# Update EMA parameters
|
| 148 |
-
with torch.no_grad():
|
| 149 |
-
self.updates += 1
|
| 150 |
-
d = self.decay(self.updates)
|
| 151 |
-
|
| 152 |
-
msd = model.module.state_dict() if is_parallel(model) else model.state_dict() # model state_dict
|
| 153 |
-
for k, v in self.ema.state_dict().items():
|
| 154 |
-
if v.dtype.is_floating_point:
|
| 155 |
-
v *= d
|
| 156 |
-
v += (1. - d) * msd[k].detach()
|
| 157 |
-
|
| 158 |
-
def update_attr(self, model, include=(), exclude=('process_group', 'reducer')):
|
| 159 |
-
# Update EMA attributes
|
| 160 |
-
copy_attr(self.ema, model, include, exclude)
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
class DataLoaderX(DataLoader):
|
| 164 |
-
"""prefetch dataloader"""
|
| 165 |
-
def __iter__(self):
|
| 166 |
-
return BackgroundGenerator(super().__iter__())
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
class AverageMeter(object):
|
| 170 |
-
"""Computes and stores the average and current value"""
|
| 171 |
-
|
| 172 |
-
def __init__(self):
|
| 173 |
-
self.val = 0
|
| 174 |
-
self.avg = 0
|
| 175 |
-
self.sum = 0
|
| 176 |
-
self.count = 0
|
| 177 |
-
|
| 178 |
-
def reset(self):
|
| 179 |
-
self.val = 0
|
| 180 |
-
self.avg = 0
|
| 181 |
-
self.sum = 0
|
| 182 |
-
self.count = 0
|
| 183 |
-
|
| 184 |
-
def update(self, val, n=1):
|
| 185 |
-
self.val = val
|
| 186 |
-
self.sum += val * n
|
| 187 |
-
self.count += n
|
| 188 |
-
self.avg = self.sum / self.count if self.count != 0 else 0
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
def git_describe(path=Path(__file__).parent): # path must be a directory
|
| 192 |
-
# return human-readable git description, i.e. v5.0-5-g3e25f1e https://git-scm.com/docs/git-describe
|
| 193 |
-
s = f'git -C {path} describe --tags --long --always'
|
| 194 |
-
try:
|
| 195 |
-
return subprocess.check_output(s, shell=True, stderr=subprocess.STDOUT).decode()[:-1]
|
| 196 |
-
except subprocess.CalledProcessError as e:
|
| 197 |
-
return '' # not a git repository
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
def date_modified(path=__file__):
|
| 201 |
-
# return human-readable file modification date, i.e. '2021-3-26'
|
| 202 |
-
t = datetime.datetime.fromtimestamp(Path(path).stat().st_mtime)
|
| 203 |
-
return f'{t.year}-{t.month}-{t.day}'
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
def is_parallel(model):
|
| 207 |
-
return type(model) in (nn.parallel.DataParallel, nn.parallel.DistributedDataParallel)
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
def copy_attr(a, b, include=(), exclude=()):
|
| 211 |
-
# Copy attributes from b to a, options to only include [...] and to exclude [...]
|
| 212 |
-
for k, v in b.__dict__.items():
|
| 213 |
-
if (len(include) and k not in include) or k.startswith('_') or k in exclude:
|
| 214 |
-
continue
|
| 215 |
-
else:
|
| 216 |
-
setattr(a, k, v)
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
def time_synchronized():
|
| 220 |
-
# pytorch-accurate time
|
| 221 |
-
if torch.cuda.is_available():
|
| 222 |
-
torch.cuda.synchronize()
|
| 223 |
-
return time.time()
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
def profile(x, ops, n=100, device=None):
|
| 227 |
-
# profile a pytorch module or list of modules. Example usage:
|
| 228 |
-
# x = torch.randn(16, 3, 640, 640) # input
|
| 229 |
-
# m1 = lambda x: x * torch.sigmoid(x)
|
| 230 |
-
# m2 = nn.SiLU()
|
| 231 |
-
# profile(x, [m1, m2], n=100) # profile speed over 100 iterations
|
| 232 |
-
|
| 233 |
-
device = device or torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
|
| 234 |
-
x = x.to(device)
|
| 235 |
-
x.requires_grad = True
|
| 236 |
-
print(torch.__version__, device.type, torch.cuda.get_device_properties(0) if device.type == 'cuda' else '')
|
| 237 |
-
print(f"\n{'Params':>12s}{'GFLOPS':>12s}{'forward (ms)':>16s}{'backward (ms)':>16s}{'input':>24s}{'output':>24s}")
|
| 238 |
-
for m in ops if isinstance(ops, list) else [ops]:
|
| 239 |
-
m = m.to(device) if hasattr(m, 'to') else m # device
|
| 240 |
-
m = m.half() if hasattr(m, 'half') and isinstance(x, torch.Tensor) and x.dtype is torch.float16 else m # type
|
| 241 |
-
dtf, dtb, t = 0., 0., [0., 0., 0.] # dt forward, backward
|
| 242 |
-
try:
|
| 243 |
-
flops = thop.profile(m, inputs=(x,), verbose=False)[0] / 1E9 * 2 # GFLOPS
|
| 244 |
-
except:
|
| 245 |
-
flops = 0
|
| 246 |
-
|
| 247 |
-
for _ in range(n):
|
| 248 |
-
t[0] = time_synchronized()
|
| 249 |
-
y = m(x)
|
| 250 |
-
t[1] = time_synchronized()
|
| 251 |
-
try:
|
| 252 |
-
_ = y.sum().backward()
|
| 253 |
-
t[2] = time_synchronized()
|
| 254 |
-
except: # no backward method
|
| 255 |
-
t[2] = float('nan')
|
| 256 |
-
dtf += (t[1] - t[0]) * 1000 / n # ms per op forward
|
| 257 |
-
dtb += (t[2] - t[1]) * 1000 / n # ms per op backward
|
| 258 |
-
|
| 259 |
-
s_in = tuple(x.shape) if isinstance(x, torch.Tensor) else 'list'
|
| 260 |
-
s_out = tuple(y.shape) if isinstance(y, torch.Tensor) else 'list'
|
| 261 |
-
p = sum(list(x.numel() for x in m.parameters())) if isinstance(m, nn.Module) else 0 # parameters
|
| 262 |
-
print(f'{p:12}{flops:12.4g}{dtf:16.4g}{dtb:16.4g}{str(s_in):>24s}{str(s_out):>24s}')
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
def initialize_weights(model):
|
| 266 |
-
for m in model.modules():
|
| 267 |
-
t = type(m)
|
| 268 |
-
if t is nn.Conv2d:
|
| 269 |
-
pass # nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
|
| 270 |
-
elif t is nn.BatchNorm2d:
|
| 271 |
-
m.eps = 1e-3
|
| 272 |
-
m.momentum = 0.03
|
| 273 |
-
elif t in [nn.Hardswish, nn.LeakyReLU, nn.ReLU, nn.ReLU6]:
|
| 274 |
-
m.inplace = True
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
def fuse_conv_and_bn(conv, bn):
|
| 278 |
-
# Fuse convolution and batchnorm layers https://tehnokv.com/posts/fusing-batchnorm-and-conv/
|
| 279 |
-
fusedconv = nn.Conv2d(conv.in_channels,
|
| 280 |
-
conv.out_channels,
|
| 281 |
-
kernel_size=conv.kernel_size,
|
| 282 |
-
stride=conv.stride,
|
| 283 |
-
padding=conv.padding,
|
| 284 |
-
groups=conv.groups,
|
| 285 |
-
bias=True).requires_grad_(False).to(conv.weight.device)
|
| 286 |
-
|
| 287 |
-
# prepare filters
|
| 288 |
-
w_conv = conv.weight.clone().view(conv.out_channels, -1)
|
| 289 |
-
w_bn = torch.diag(bn.weight.div(torch.sqrt(bn.eps + bn.running_var)))
|
| 290 |
-
fusedconv.weight.copy_(torch.mm(w_bn, w_conv).view(fusedconv.weight.shape))
|
| 291 |
-
|
| 292 |
-
# prepare spatial bias
|
| 293 |
-
b_conv = torch.zeros(conv.weight.size(0), device=conv.weight.device) if conv.bias is None else conv.bias
|
| 294 |
-
b_bn = bn.bias - bn.weight.mul(bn.running_mean).div(torch.sqrt(bn.running_var + bn.eps))
|
| 295 |
-
fusedconv.bias.copy_(torch.mm(w_bn, b_conv.reshape(-1, 1)).reshape(-1) + b_bn)
|
| 296 |
-
|
| 297 |
-
return fusedconv
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
def model_info(model, verbose=False, img_size=640):
|
| 301 |
-
# Model information. img_size may be int or list, i.e. img_size=640 or img_size=[640, 320]
|
| 302 |
-
n_p = sum(x.numel() for x in model.parameters()) # number parameters
|
| 303 |
-
n_g = sum(x.numel() for x in model.parameters() if x.requires_grad) # number gradients
|
| 304 |
-
if verbose:
|
| 305 |
-
print('%5s %40s %9s %12s %20s %10s %10s' % ('layer', 'name', 'gradient', 'parameters', 'shape', 'mu', 'sigma'))
|
| 306 |
-
for i, (name, p) in enumerate(model.named_parameters()):
|
| 307 |
-
name = name.replace('module_list.', '')
|
| 308 |
-
print('%5g %40s %9s %12g %20s %10.3g %10.3g' %
|
| 309 |
-
(i, name, p.requires_grad, p.numel(), list(p.shape), p.mean(), p.std()))
|
| 310 |
-
|
| 311 |
-
try: # FLOPS
|
| 312 |
-
from thop import profile
|
| 313 |
-
stride = max(int(model.stride.max()), 32) if hasattr(model, 'stride') else 32
|
| 314 |
-
img = torch.zeros((1, model.yaml.get('ch', 3), stride, stride), device=next(model.parameters()).device) # input
|
| 315 |
-
flops = profile(deepcopy(model), inputs=(img,), verbose=False)[0] / 1E9 * 2 # stride GFLOPS
|
| 316 |
-
img_size = img_size if isinstance(img_size, list) else [img_size, img_size] # expand if int/float
|
| 317 |
-
fs = ', %.1f GFLOPS' % (flops * img_size[0] / stride * img_size[1] / stride) # 640x640 GFLOPS
|
| 318 |
-
except (ImportError, Exception):
|
| 319 |
-
fs = ''
|
| 320 |
-
|
| 321 |
-
logger.info(f"Model Summary: {len(list(model.modules()))} layers, {n_p} parameters, {n_g} gradients{fs}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|