Table des matières
Générer des données pour la détection de cercles et de rectangles
introduction
Plusieurs versions ont été itérées depuis sa sortie le 18 mai 2020. La dernière version est la v7, qui ajoute des capacités de segmentation. Il y a eu de nombreux articles de blog expliquant le principe de yolov5 et comment utiliser les données marquées, telles que l' explication détaillée du réseau YOLOv5 et l'explication approfondie des connaissances de base de base de Yolov5 dans la série Yolo.
Simple à installer et facile à utiliser, il est devenu de facto la référence des méthodes de détection
// 克隆代码库即可
git clone https://github.com/ultralytics/yolov5 # clone
cd yolov5
pip install -r requirements.txt # install
Il suffit d'une ligne de code pour terminer lors de l'utilisation
import torch
# 加载模型
model = torch.hub.load('ultralytics/yolov5', 'yolov5s') # or yolov5n - yolov5x6, custom
# 图片路径
img = 'https://ultralytics.com/images/zidane.jpg' # or file, Path, PIL, OpenCV, numpy, list
# 执行检测推理
results = model(img)
# 检测结果可视化
results.print() # or .show(), .save(), .crop(), .pandas(), etc.
Quoi? Vous ne voulez pas écrire une seule ligne de code. Si vous souhaitez développer sans code et voir l'effet directement depuis la caméra, l'exécution de detect.py dans l'entrepôt peut également répondre à vos besoins.
python detect.py --weights yolov5s.pt --source 0
La signification des paramètres détaillés est la suivante, où --weights spécifie le poids de pré-entraînement que vous souhaitez utiliser, --source spécifie la source à détecter (image, liste de chemin d'image, caméra ou même flux push réseau)
python detect.py --weights yolov5s.pt --source 0 # webcam
img.jpg # image
vid.mp4 # video
screen # screenshot
path/ # directory
list.txt # list of images
list.streams # list of streams
'path/*.jpg' # glob
'https://youtu.be/Zgi9g1ksQHc' # YouTube
'rtsp://example.com/media.mp4' # RTSP, RTMP, HTTP stream
organisation du réseau
YOLOv5 a la même architecture réseau globale pour différentes tailles ( n
, s
, m
, l
, ), mais il utilise différentes profondeurs et largeurs dans chaque sous-module pour traiter respectivement les paramètres du fichier . Il convient également de noter qu'en plus de la version officielle , , , , il existe également , , , , la différence est que cette dernière est destinée aux images avec des résolutions plus grandes. Par exemple , il existe bien sûr quelques différences de structure. 4 couches d'entités de prédiction, tandis que le premier ne sous-échantillonnera que 32 fois et utilisera 3 couches d'entités de prédiction.x
yaml
depth_multiple
width_multiple
n
s
m
l
x
n6
s6
m6
l6
x6
1280x1280
Par rapport à la version précédente, YOLOv5
v6.0
a un petit changement après la version, remplaçant la première couche du réseau (à l'origine unFocus
module) par une6x6
grande et une petite couche convolutive. Les deux sont équivalents en théorie , mais pour certains dispositifs GPU existants (et les algorithmes d'optimisation correspondants), il est plus efficace d'utiliser6x6
de grandes et petites couches convolutives que d'utiliserFocus
des modules. Pour plus de détails, veuillez consulter ce numéro #4825. La figure ci-dessous est leFocus
module d'origine (similaire au précédentSwin Transformer
)Patch Merging
,2x2
divisez chaque pixel adjacent en unpatch
, puispatch
assemblez les pixels de même position (même couleur) dans chacun pour obtenir 4feature map
, puis connectez Une3x3
couche convolutive de la taille précédente . Cela équivaut à utiliser directement6x6
une couche convolutive d' une taille.
La partie Neck sera
SPP
remplacée par uneSPPF
(Glenn Jocher
auto-conçue), la fonction des deux est la même, mais cette dernière est plus efficace.SPP
La structure consiste à faire passer l'entrée à travers plusieurs tailles différentes en parallèleMaxPool
, puis à effectuer une fusion supplémentaire, ce qui peut résoudre le problème multi-échelle cible dans une certaine mesure. LaSPPF
structure consiste à sérialiser l'entrée à travers des couches5x5
de plusieurs taillesMaxPool
. Il convient de noter ici que le résultat du calcul de la sérialisation des couches5x5
de deux tailles est le même que celui des couches d' une taille, et le résultat du calcul de la sérialisation des couches de trois tailles est identique à celui des calques d'une même taille. Les résultats du calcul des calques sont les mêmes.MaxPool
9x9
MaxPool
5x5
MaxPool
13x13
MaxPool
augmentation des données
Mosaïque , combinez quatre images en une seule image
Copiez-collez , collez au hasard certaines cibles dans l'image, le principe est que les données doivent avoir segments
des données, c'est-à-dire les informations de segmentation d'instance de chaque cible
Random affine (Rotation, Scale, Translation and Shear) effectue une transformation affine de manière aléatoire, mais selon les hyperparamètres du fichier de configuration, il s'avère que seules la somme Scale
et Translation
la traduction sont utilisées.
MixUp consiste à mélanger deux images ensemble avec une certaine transparence. On ne sait pas si c'est utile ou non. Après tout, il n'y a pas de papiers et pas d'expériences d'ablation. Seul le plus grand modèle est utilisé dans le code MixUp
, et seulement 10 % du temps.
Albumentations , principalement pour faire du filtrage, de l'égalisation de l'histogramme et changer la qualité de l'image, etc. Je vois que le code écrit dans le code ne sera activé que lorsque le package sera installé , mais le package est commenté albumentations
dans le requirements.txt
fichier de projet , donc il n'est pas activé par défaut albumentations
.
Augmenter HSV (Teinte, Saturation, Valeur) ajuste de manière aléatoire la teinte, la saturation et la luminosité.
Retournement horizontal aléatoire , retournement horizontal aléatoire
De nombreuses stratégies de formation sont utilisées dans le code source YOLOv5
- Formation multi-échelle (0,5 ~ 1,5x) , formation multi-échelle, en supposant que la taille de l'image d'entrée est définie sur 640 × 640, la taille utilisée pendant la formation est sélectionnée au hasard entre 0,5 × 640 ∼ 1,5 × 640, faites attention à la valeur obtenue lors de la sélection de la valeur Les deux sont des multiples entiers de 32 (car le réseau sous-échantillonnera au maximum 32 fois).
- AutoAnchor (pour la formation de données personnalisées) , lors de la formation de votre propre ensemble de données, vous pouvez vous regrouper pour générer des modèles d'ancres en fonction des objectifs de votre propre ensemble de données.
- Planificateur d'échauffement et de cosinus LR , échauffez-vous avant l'entraînement
Warmup
, puis utilisezCosine
la stratégie de baisse du taux d'apprentissage.- L'EMA (moyenne mobile exponentielle) peut être comprise comme l'ajout d'un élan aux paramètres d'entraînement pour rendre le processus de mise à jour plus fluide.
- Précision mixte , formation de précision mixte, peut réduire l'utilisation de la mémoire vidéo et accélérer la formation, à condition que le support matériel GPU.
- Faire évoluer les hyper-paramètres , l'optimisation des hyperparamètres, les personnes qui n'ont aucune expérience en alchimie ne doivent pas y toucher, juste garder la valeur par défaut.
La perte de YOLOv5 se compose principalement de trois parties :
- La perte de classes , la perte de classification, est utilisée
BCE loss
, faites attention à ne calculer que la perte de classification des échantillons positifs. - Objectness loss ,
obj
la perte, est toujours utiliséBCE loss
Notez que celaobj
fait référence à la boîte englobante cible prédite par le réseau et la GT BoxCIoU
. Ce qui est calculé ici estobj
la perte de tous les échantillons. - La perte de localisation , la perte de localisation, est utilisée
CIoU loss
, faites attention à ne calculer que la perte de localisation des échantillons positifs.
déployer
Les versions antérieures à yolov5 v6.0 (non incluses) utilisent la couche Focus, ce qui entraîne de nombreuses modifications du déploiement et nécessite de nombreuses opérations compliquées . les étapes sont les suivantes Détecter YOLOv5 vers le déploiement
// 1.导出onnx
python models/export.py --weights yolov5s.pt --img 320 --batch 1
// 2.简化模型
python -m onnxsim yolov5s.onnx yolov5s-sim.onnx
// 3. 模型转换到ncnn
./onnx2ncnn yolov5s-sim.onnx yolov5s.param yolov5s.bin
// 4. 编辑 yolov5s.param文件
第4行到13行删除(也就是Slice和Concat层),将第二行由172改成164(一共删除了10层,第二行的173更改为164,计算方法173-(10-1)=164)
增加自定义层
YoloV5Focus focus 1 1 images 159
其中159是刚才删除的Concat层的输出
// 5. 支持动态尺寸输入
将reshape中的960,240,60更改为-1,或者其他 0=后面的数
// 6. ncnnoptimize优化
./ncnnoptimize yolov5s.param yolov5s.bin yolov5s-opt.param yolov5s-opt.bin 1
Après la v6.0, il est beaucoup plus pratique d'utiliser la convolution 6x6 à la place.Vous pouvez directement utiliser le module dnn d'opencv pour le déploiement.Pour plus de détails, voir Détection d'objets avec YOLOv5, OpenCV, Python et C++ , code yolov5-opencv-cpp-python
Cependant, il convient de noter qu'il ne peut coopérer qu'avec opencv4.5.5 et supérieur.Il comprend principalement 6 étapes
// 1.加载模型
net = cv2.dnn.readNet('yolov5s.onnx')
// 2.加载图片
def format_yolov5(source):
# put the image in square big enough
col, row, _ = source.shape
_max = max(col, row)
resized = np.zeros((_max, _max, 3), np.uint8)
resized[0:col, 0:row] = source
# resize to 640x640, normalize to [0,1[ and swap Red and Blue channels
result = cv2.dnn.blobFromImage(resized, 1/255.0, (640, 640), swapRB=True)
return result
// 3.执行推理
predictions = net.forward()
output = predictions[0]
// 4.展开结果
def unwrap_detection(input_image, output_data):
class_ids = []
confidences = []
boxes = []
rows = output_data.shape[0]
image_width, image_height, _ = input_image.shape
x_factor = image_width / 640
y_factor = image_height / 640
for r in range(rows):
row = output_data[r]
confidence = row[4]
if confidence >= 0.4:
classes_scores = row[5:]
_, _, _, max_indx = cv2.minMaxLoc(classes_scores)
class_id = max_indx[1]
if (classes_scores[class_id] > .25):
confidences.append(confidence)
class_ids.append(class_id)
x, y, w, h = row[0].item(), row[1].item(), row[2].item(), row[3].item()
left = int((x - 0.5 * w) * x_factor)
top = int((y - 0.5 * h) * y_factor)
width = int(w * x_factor)
height = int(h * y_factor)
box = np.array([left, top, width, height])
boxes.append(box)
// 5.非极大值抑制
indexes = cv2.dnn.NMSBoxes(boxes, confidences, 0.25, 0.45)
result_class_ids = []
result_confidences = []
result_boxes = []
for i in indexes:
result_confidences.append(confidences[i])
result_class_ids.append(class_ids[i])
result_boxes.append(boxes[I])
// 6.可视化结果输出
class_list = []
with open("classes.txt", "r") as f:
class_list = [cname.strip() for cname in f.readlines()]
colors = [(255, 255, 0), (0, 255, 0), (0, 255, 255), (255, 0, 0)]
for i in range(len(result_class_ids)):
box = result_boxes[i]
class_id = result_class_ids[i]
color = colors[class_id % len(colors)]
conf = result_confidences[i]
cv2.rectangle(image, box, color, 2)
cv2.rectangle(image, (box[0], box[1] - 20), (box[0] + box[2], box[1]), color, -1)
cv2.putText(image, class_list[class_id], (box[0] + 5, box[1] - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,0,0))
Générer des données pour la détection de cercles et de rectangles
Ensuite, un projet de détection de cercles et de rectangles montre la génération de données d'entraînement et le processus d'entraînement de yolov5. Mais l'étiquetage des données est une tâche qui prend du temps et demande beaucoup de main-d'œuvre. Ne serait-il pas formidable que les données générées puissent être utilisées pour vérifier rapidement certaines expériences ?
Basé sur Lable Studio [enregistrements de combat réels yoloV5] Xiaobai peut également former son propre ensemble de données ! Sur la base de labelimg, je vais vous apprendre à utiliser le deep learning pour faire de la détection d'objets (2) : étiquetage des données - Programmeur
Le format d'étiquetage de Yolov5 est très simple. Les images sont placées dans le dossier images. Sous le dossier étiquettes, chaque fichier image a un fichier txt correspondant avec le même nom, qui stocke la catégorie, les coordonnées normalisées et la largeur de chaque cible par ligne Élevé, de nombreux outils d'annotation prennent en charge l'exportation directe du format d'annotation yolo, et il existe également de nombreux scripts qui peuvent facilement convertir des formats VOC, coco et autres au format YOLO.
类别1 归一化中心点坐标x 归一化中心坐标y 归一化宽度 归一化高度
类别2 归一化中心点坐标x 归一化中心坐标y 归一化宽度 归一化高度
1. Ici, nous prenons le cercle de détection comme exemple pour introduire chaque étape en détail
Le premier est la génération et la visualisation des données d'entraînement, dessinez au hasard un cercle avec un point aléatoire comme centre et un rayon de 60-100 comme cible que nous voulons détecter, et générez un total de 100 000 données d'entraînement
import os
import cv2
import math
import random
import numpy as np
from tqdm import tqdm
def generate():
img = np.zeros((640,640,3),np.uint8)
x = 100+random.randint(0, 400)
y = 100+random.randint(0, 400)
radius = random.randint(60,100)
r = random.randint(0,255)
g = random.randint(0,255)
b = random.randint(0,255)
cv2.circle(img, (x,y), radius, (b,g,r),-1)
return img, [x,y,radius]
def generate_batch(num=10000):
images_dir = "data/circle/images"
if not os.path.exists(images_dir):
os.makedirs(images_dir)
labels_dir = "data/circle/labels"
if not os.path.exists(labels_dir):
os.makedirs(labels_dir)
for i in tqdm(range(num)):
img, labels = generate()
cv2.imwrite(images_dir+"/"+str(i)+".jpg", img)
with open(labels_dir+"/"+str(i)+".txt", 'w') as f:
x, y, radius = labels
f.write("0 "+str(x/640)+" "+str(y/640)+" "+str(2*radius/640)+" "+str(2*radius/640)+"\n")
def show_gt(dir='data/circle'):
files = os.listdir(dir+"/images")
gtdir = dir+"/gt"
if not os.path.exists(gtdir):
os.makedirs(gtdir)
for file in tqdm(files):
imgpath = dir+"/images/"+file
img = cv2.imread(imgpath)
h,w,_ = img.shape
labelpath = dir+"/labels/"+file[:-3]+"txt"
with open(labelpath) as f:
lines = f.readlines()
for line in lines:
items = line[:-1].split(" ")
c = int(items[0])
cx = float(items[1])
cy = float(items[2])
cw = float(items[3])
ch = float(items[4])
x1 = int((cx - cw/2)*w)
y1 = int((cy - ch/2)*h)
x2 = int((cx + cw/2)*w)
y2 = int((cy + ch/2)*h)
cv2.rectangle(img, (x1,y1),(x2,y2),(0,255,0),2)
cv2.imwrite(gtdir+"/"+file, img)
if __name__=="__main__":
generate_batch()
show_gt()
Puis construisez circle.yaml
train: data/circle/images/
val: data/circle/images/
# number of classes
nc: 1
# class names
names: ['circle']
2. Si vous souhaitez détecter des cibles circulaires et rectangulaires, vous devez ajuster le script de génération et le fichier de configuration des données
import os
import cv2
import math
import random
import numpy as np
from tqdm import tqdm
def generate_circle():
img = np.zeros((640,640,3),np.uint8)
x = 100+random.randint(0, 400)
y = 100+random.randint(0, 400)
radius = random.randint(60,100)
r = random.randint(0,255)
g = random.randint(0,255)
b = random.randint(0,255)
cv2.circle(img, (x,y), radius, (b,g,r),-1)
return img, [x,y,radius*2,radius*2]
def generate_rectangle():
img = np.zeros((640,640,3),np.uint8)
x1 = 100+random.randint(0, 400)
y1 = 100+random.randint(0, 400)
w = random.randint(80, 200)
h = random.randint(80, 200)
x2 = x1 + w
y2 = y1 + h
r = random.randint(0,255)
g = random.randint(0,255)
b = random.randint(0,255)
cx = (x1+x2)//2
cy = (y1+y2)//2
cv2.rectangle(img, (x1,y1), (x2,y2), (b,g,r),-1)
return img, [cx,cy,w,h]
def generate_batch(num=100000):
images_dir = "data/shape/images"
if not os.path.exists(images_dir):
os.makedirs(images_dir)
labels_dir = "data/shape/labels"
if not os.path.exists(labels_dir):
os.makedirs(labels_dir)
for i in tqdm(range(num)):
if i % 2 == 0:
img, labels = generate_circle()
else:
img, labels = generate_rectangle()
cv2.imwrite(images_dir+"/"+str(i)+".jpg", img)
with open("data/shape/labels/"+str(i)+".txt", 'w') as f:
cx,cy,w,h = labels
f.write(str(i%2)+" "+str(cx/640)+" "+str(cy/640)+" "+str(w/640)+" "+str(h/640)+"\n")
def show_gt(dir='data/shape'):
files = os.listdir(dir+"/images")
gtdir = dir+"/gt"
if not os.path.exists(gtdir):
os.makedirs(gtdir)
for file in tqdm(files):
imgpath = dir+"/images/"+file
img = cv2.imread(imgpath)
h, w, _ = img.shape
labelpath = dir+"/labels/"+file[:-3]+"txt"
with open(labelpath) as f:
lines = f.readlines()
for line in lines:
items = line[:-1].split(" ")
c = int(items[0])
cx = float(items[1])
cy = float(items[2])
cw = float(items[3])
ch = float(items[4])
x1 = int((cx - cw/2)*w)
y1 = int((cy - ch/2)*h)
x2 = int((cx + cw/2)*w)
y2 = int((cy + ch/2)*h)
cv2.rectangle(img, (x1,y1),(x2,y2),(0,255,0),2)
cv2.putText(img, str(c), (x1,y1), 3,1,(0,0,255))
cv2.imwrite(gtdir+"/"+file, img)
if __name__=="__main__":
generate_batch()
show_gt()
Shape.yaml correspondant, notez que le nombre de catégories est de 2
train: data/shape/images/
val: data/shape/images/
# number of classes
nc: 2
# class names
names: ['circle', 'rectangle']
former
Commencez l'entraînement avec la commande suivante
python train.py --data circle.yaml --cfg yolov5s.yaml --weights '' --batch-size 64
S'il y a deux types de cibles, circulaire et rectangulaire, la commande est
python train.py --data shape.yaml --cfg yolov5s.yaml --weights '' --batch-size 64
Regardez les statistiques, les catégories et les distributions imprimées pendant la formation
Entraînez quelques époques pour voir les résultats
epoch, train/box_loss, train/obj_loss, train/cls_loss, metrics/precision, metrics/recall, metrics/mAP_0.5,metrics/mAP_0.5:0.95, val/box_loss, val/obj_loss, val/cls_loss, x/lr0, x/lr1, x/lr2
0, 0.03892, 0.011817, 0, 0.99998, 0.99978, 0.995, 0.92987, 0.0077891, 0.0030948, 0, 0.0033312, 0.0033312, 0.070019
1, 0.017302, 0.0049876, 0, 1, 0.9999, 0.995, 0.99105, 0.0031843, 0.0015662, 0, 0.0066644, 0.0066644, 0.040019
2, 0.011272, 0.0034826, 0, 1, 0.99994, 0.995, 0.99499, 0.0020194, 0.0010969, 0, 0.0099969, 0.0099969, 0.010018
3, 0.0080153, 0.0027186, 0, 1, 0.99994, 0.995, 0.995, 0.0013095, 0.00083033, 0, 0.0099978, 0.0099978, 0.0099978
4, 0.0067639, 0.0023831, 0, 1, 0.99996, 0.995, 0.995, 0.00099513, 0.00068878, 0, 0.0099978, 0.0099978, 0.0099978
5, 0.0061637, 0.0022279, 0, 1, 0.99996, 0.995, 0.995, 0.00090497, 0.00064193, 0, 0.0099961, 0.0099961, 0.0099961
6, 0.0058844, 0.002144, 0, 0.99999, 0.99998, 0.995, 0.995, 0.0009117, 0.00063328, 0, 0.0099938, 0.0099938, 0.0099938
7, 0.0056247, 0.00208, 0, 0.99999, 0.99999, 0.995, 0.995, 0.00086355, 0.00061343, 0, 0.0099911, 0.0099911, 0.0099911
8, 0.0054567, 0.0020223, 0, 1, 0.99999, 0.995, 0.995, 0.00081632, 0.00059592, 0, 0.0099879, 0.0099879, 0.0099879
9, 0.0053597, 0.0019864, 0, 1, 1, 0.995, 0.995, 0.00081379, 0.00058942, 0, 0.0099842, 0.0099842, 0.0099842
10, 0.0053103, 0.0019559, 0, 1, 1, 0.995, 0.995, 0.0008175, 0.00058669, 0, 0.00998, 0.00998, 0.00998
11, 0.0052146, 0.0019445, 0, 1, 1, 0.995, 0.995, 0.00083248, 0.00058731, 0, 0.0099753, 0.0099753, 0.0099753
12, 0.0050852, 0.0019065, 0, 1, 1, 0.995, 0.995, 0.00085092, 0.00058853, 0, 0.0099702, 0.0099702, 0.0099702
13, 0.0050589, 0.0019031, 0, 1, 1, 0.995, 0.995, 0.00086915, 0.00059267, 0, 0.0099645, 0.0099645, 0.0099645
14, 0.0049664, 0.0018693, 0, 1, 1, 0.995, 0.995, 0.00090856, 0.00059815, 0, 0.0099584, 0.0099584, 0.0099584
15, 0.0049839, 0.0018568, 0, 1, 1, 0.995, 0.995, 0.00093147, 0.00060425, 0, 0.0099517, 0.0099517, 0.0099517
16, 0.0049079, 0.0018459, 0, 1, 1, 0.995, 0.995, 0.0009656, 0.00061124, 0, 0.0099446, 0.0099446, 0.0099446
17, 0.0048693, 0.0018277, 0, 1, 1, 0.995, 0.995, 0.00099703, 0.00061948, 0, 0.009937, 0.009937, 0.009937
18, 0.0048052, 0.0018103, 0, 1, 1, 0.995, 0.995, 0.0010246, 0.00062618, 0, 0.0099289, 0.0099289, 0.0099289
19, 0.0047608, 0.0017947, 0, 1, 1, 0.995, 0.995, 0.0010439, 0.00063123, 0, 0.0099203, 0.0099203, 0.0099203
Le mAP a atteint 99,5+, ce qui est vraiment bien, regardez les résultats de la prédiction
objets ronds et rectangulaires
déployer
Enfin, utilisez la commande suivante pour détecter, n'oubliez pas de remplacer le chemin par le chemin local
python detect.py --weights exps/yolov5s_circle/weights/best.pt --source data/circle/images
La démo intégrée est trop longue pour être compatible avec différents formats, et le code de déploiement onnx est beaucoup plus simple
import cv2
import numpy as np
import torch
from torchvision import transforms
import onnxruntime
from utils.general import non_max_suppression
def detect(img, ort_session):
img = img.astype(np.float32)
img = img / 255
img_tensor = img.transpose(2,0,1)[None]
ort_inputs = {ort_session.get_inputs()[0].name: img_tensor}
pred = torch.tensor(ort_session.run(None, ort_inputs)[0])
dets = non_max_suppression(pred, 0.25, 0.45)
return dets[0]
def demo():
ort_session = onnxruntime.InferenceSession("yolov5s.onnx", providers=['TensorrtExecutionProvider'])
img = cv2.imread("data/images/bus.jpg")
img = cv2.resize(img,(640,640))
dets = detect(img, ort_session)
for det in dets:
x1 = int(det[0])
y1 = int(det[1])
x2 = int(det[2])
y2 = int(det[3])
score = float(det[4])
cls = int(det[5])
info = "{}_{:.2f}".format(cls, score*100)
cv2.rectangle(img, (x1,y1),(x2,y2),(255,255,0))
cv2.putText(img, info, (x1,y1), 1, 1, (0,0,255))
cv2.imwrite("runs/detect/bus.jpg", img)
if __name__=="__main__":
demo()
Résumer
Cet article explique en détail comment générer les étiquettes requises pour les données d'entraînement à travers deux exemples de détection de cercle et de détection de rectangle, et donne l'implémentation du code de l'ensemble du processus d'entraînement, de test et de déploiement