From 011e49dd0ae403cb116a2f9fd27abd80533b98a7 Mon Sep 17 00:00:00 2001 From: huanxiaoling <3174348550@qq.com> Date: Tue, 18 Oct 2022 13:38:36 +0800 Subject: [PATCH] update en files --- .../image_classfication_dataset_process.md | 452 ++++++++++ docs/federated/docs/source_en/index.rst | 1 + .../image_classfication_dataset_process.md | 12 +- ...lassification_application_in_cross_silo.md | 4 +- docs/mindspore/source_en/index.rst | 3 + .../analysis_and_preparation.md | 475 ++++++++++ .../migration_guide/debug_and_tune.md | 296 ++++++ .../images/evaluation_procession.png | Bin 0 -> 23286 bytes .../images/parameter_freeze.png | Bin 0 -> 34703 bytes .../images/train_procession.png | Bin 0 -> 39117 bytes .../model_development/model_and_loss.md | 702 +++++++++++++++ .../model_development/model_development.md | 8 +- .../training_and_evaluation_procession.md | 2 +- .../source_en/migration_guide/overview.md | 10 +- .../source_en/migration_guide/sample_code.md | 840 ++++++++++++++++++ .../typical_api_comparision.md | 396 +++++++++ .../migration_guide/use_third_party_op.md | 2 +- .../migration_guide/enveriment_preparation.md | 2 +- 18 files changed, 3187 insertions(+), 18 deletions(-) create mode 100644 docs/federated/docs/source_en/image_classfication_dataset_process.md create mode 100644 docs/mindspore/source_en/migration_guide/analysis_and_preparation.md create mode 100644 docs/mindspore/source_en/migration_guide/debug_and_tune.md create mode 100644 docs/mindspore/source_en/migration_guide/model_development/images/evaluation_procession.png create mode 100644 docs/mindspore/source_en/migration_guide/model_development/images/parameter_freeze.png create mode 100644 docs/mindspore/source_en/migration_guide/model_development/images/train_procession.png create mode 100644 docs/mindspore/source_en/migration_guide/model_development/model_and_loss.md create mode 100644 docs/mindspore/source_en/migration_guide/sample_code.md create mode 100644 docs/mindspore/source_en/migration_guide/typical_api_comparision.md diff --git a/docs/federated/docs/source_en/image_classfication_dataset_process.md b/docs/federated/docs/source_en/image_classfication_dataset_process.md new file mode 100644 index 0000000000..07cca7214f --- /dev/null +++ b/docs/federated/docs/source_en/image_classfication_dataset_process.md @@ -0,0 +1,452 @@ +# Federated Learning Image Classification Dataset Process + + + +This tutorial uses the federated learning dataset `FEMNIST` in the `leaf` dataset, which contains 62 different categories of handwritten digits and letters (digits 0 to 9, 26 lowercase letters, and 26 uppercase letters) with an image size of `28 x 28` pixels. The dataset contains handwritten digits and letters from 3500 users (up to 3500 clients can be simulated to participate in federated learning). The total data volume is 805,263, the average data volume per user is 226.83, and the variance of the data volume for all users is 88.94. + +## Device-Cloud Federated Learning Image Classification Dataset Process + +Refer to [leaf dataset instruction](https://github.com/TalwalkarLab/leaf) to download the dataset. + +1. Environmental requirements before downloading the dataset. + + ```sh + numpy==1.16.4 + scipy # conda install scipy + tensorflow==1.13.1 # pip install tensorflow + Pillow # pip install Pillow + matplotlib # pip install matplotlib + jupyter # conda install jupyter notebook==5.7.8 tornado==4.5.3 + pandas # pip install pandas + ``` + +2. Use git to download the official dataset generation script. + + ```sh + git clone https://github.com/TalwalkarLab/leaf.git + ``` + + After downloading the project, the directory structure is as follows: + + ```sh + leaf/data/femnist + ├── data # Used to store the dataset generated by the command + ├── preprocess # Store the code related to data pre-processing + ├── preprocess.sh # shell script generated by femnist dataset + └── README.md # Official dataset download guidance + ``` + +3. Taking `femnist` dataset as an example, run the following command to enter the specified path. + + ```sh + cd leaf/data/femnist + ``` + +4. Using the command `. /preprocess.sh -s niid --sf 1.0 -k 0 -t sample` generates a dataset containing 3500 users, and the training sets and the test sets are divided in a ratio of 9:1 for each user's data. + + The meaning of the parameters in the command can be found in the `leaf/data/femnist/README.md` file. + + The directory structure after running is as follows: + + ```text + leaf/data/femnist/35_client_sf1_data/ + ├── all_data # All datasets are mixed together, without distinguishing the training sets and test sets, containing a total of 35 json files, and each json file contains the data of 100 users + ├── test # The test sets are divided into the training sets and the test sets in a ratio of 9:1 for each user's data, containing a total of 35 json files, and each json file contains the data of 100 users + ├── train # The training sets are divided into the training sets and the test sets in a ratio of 9:1 for each user's data, containing a total of 35 json files, and each json file contains the data of 100 users + └── ... # Other documents do not need to use, and details are not described herein + ``` + + Each json file contains the following three parts: + + - `users`: User list. + - `num_samples`: The sample number list of each user. + - `user_data`: A dictionary object with user names as key and their respective data as value. For each user, the data is represented as a list of images, with each image represented as a list of integers of size 784 (obtained by spreading the `28 x 28` image array). + + Before rerunning `preprocess.sh`, make sure to delete the `rem_user_data`, `sampled_data`, `test` and `train` subfolders from the data directory. + +5. Divide the 35 json files into 3500 json files (each json file represents a user). + + The code is as follows: + + ```python + import os + import json + + def mkdir(path): + if not os.path.exists(path): + os.mkdir(path) + + def partition_json(root_path, new_root_path): + """ + partition 35 json files to 3500 json file + + Each raw .json file is an object with 3 keys: + 1. 'users', a list of users + 2. 'num_samples', a list of the number of samples for each user + 3. 'user_data', an object with user names as keys and their respective data as values; for each user, data is represented as a list of images, with each image represented as a size-784 integer list (flattened from 28 by 28) + + Each new .json file is an object with 3 keys: + 1. 'user_name', the name of user + 2. 'num_samples', the number of samples for the user + 3. 'user_data', an dict object with 'x' as keys and their respective data as values; with 'y' as keys and their respective label as values; + + Args: + root_path (str): raw root path of 35 json files + new_root_path (str): new root path of 3500 json files + """ + paths = os.listdir(root_path) + count = 0 + file_num = 0 + for i in paths: + file_num += 1 + file_path = os.path.join(root_path, i) + print('======== process ' + str(file_num) + ' file: ' + str(file_path) + '======================') + with open(file_path, 'r') as load_f: + load_dict = json.load(load_f) + users = load_dict['users'] + num_users = len(users) + num_samples = load_dict['num_samples'] + for j in range(num_users): + count += 1 + print('---processing user: ' + str(count) + '---') + cur_out = {'user_name': None, 'num_samples': None, 'user_data': {}} + cur_user_id = users[j] + cur_data_num = num_samples[j] + cur_user_path = os.path.join(new_root_path, cur_user_id + '.json') + cur_out['user_name'] = cur_user_id + cur_out['num_samples'] = cur_data_num + cur_out['user_data'].update(load_dict['user_data'][cur_user_id]) + with open(cur_user_path, 'w') as f: + json.dump(cur_out, f) + f = os.listdir(new_root_path) + print(len(f), ' users have been processed!') + # partition train json files + partition_json("leaf/data/femnist/35_client_sf1_data/train", "leaf/data/femnist/3500_client_json/train") + # partition test json files + partition_json("leaf/data/femnist/35_client_sf1_data/test", "leaf/data/femnist/3500_client_json/test") + ``` + + where `root_path` is `leaf/data/femnist/35_client_sf1_data/{train,test}`. `new_root_path` is set by itself to store the generated 3500 user json files, which need to be processed separately for the training and test folders. + + Each of the 3500 newly generated user json files contains the following three parts: + + - `user_name`: User name. + - `num_samples`: The number of user samples + - `user_data`: A dictionary object with 'x' as key and user data as value; with 'y' as key and the label corresponding to the user data as value. + + Print the result as following after running the script, which means a successful run: + + ```sh + ======== process 1 file: /leaf/data/femnist/35_client_sf1_data/train/all_data_16_niid_0_keep_0_train_9.json====================== + ---processing user: 1--- + ---processing user: 2--- + ---processing user: 3--- + ...... + ``` + +6. Convert a json file to an image file. + + Refer to the following code: + + ```python + import os + import json + import numpy as np + from PIL import Image + + name_list = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', + 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', + 'v', 'w', 'x', 'y', 'z' + ] + + def mkdir(path): + if not os.path.exists(path): + os.mkdir(path) + + def json_2_numpy(img_size, file_path): + """ + read json file to numpy + Args: + img_size (list): contain three elements: the height, width, channel of image + file_path (str): root path of 3500 json files + return: + image_numpy (numpy) + label_numpy (numpy) + """ + # open json file + with open(file_path, 'r') as load_f_train: + load_dict = json.load(load_f_train) + num_samples = load_dict['num_samples'] + x = load_dict['user_data']['x'] + y = load_dict['user_data']['y'] + size = (num_samples, img_size[0], img_size[1], img_size[2]) + image_numpy = np.array(x, dtype=np.float32).reshape(size) # mindspore doesn't support float64 and int64 + label_numpy = np.array(y, dtype=np.int32) + return image_numpy, label_numpy + + def json_2_img(json_path, save_path): + """ + transform single json file to images + + Args: + json_path (str): the path json file + save_path (str): the root path to save images + + """ + data, label = json_2_numpy([28, 28, 1], json_path) + for i in range(data.shape[0]): + img = data[i] * 255 # PIL don't support the 0/1 image ,need convert to 0~255 image + im = Image.fromarray(np.squeeze(img)) + im = im.convert('L') + img_name = str(label[i]) + '_' + name_list[label[i]] + '_' + str(i) + '.png' + path1 = os.path.join(save_path, str(label[i])) + mkdir(path1) + img_path = os.path.join(path1, img_name) + im.save(img_path) + print('-----', i, '-----') + + def all_json_2_img(root_path, save_root_path): + """ + transform json files to images + Args: + json_path (str): the root path of 3500 json files + save_path (str): the root path to save images + """ + usage = ['train', 'test'] + for i in range(2): + x = usage[i] + files_path = os.path.join(root_path, x) + files = os.listdir(files_path) + + for name in files: + user_name = name.split('.')[0] + json_path = os.path.join(files_path, name) + save_path1 = os.path.join(save_root_path, user_name) + mkdir(save_path1) + save_path = os.path.join(save_path1, x) + mkdir(save_path) + print('=============================' + name + '=======================') + json_2_img(json_path, save_path) + + all_json_2_img("leaf/data/femnist/3500_client_json/", "leaf/data/femnist/3500_client_img/") + ``` + + Print the result as following after running the script, which means a successful run: + + ```sh + =============================f0644_19.json======================= + ----- 0 ----- + ----- 1 ----- + ----- 2 ----- + ...... + ``` + +7. Since the dataset under some user folders is small, if the number is smaller than the batch size, random expansion is required. + + The entire dataset `"leaf/data/femnist/3500_client_img/"` can be checked and expanded by referring to the following code: + + ```python + import os + import shutil + from random import choice + + def count_dir(path): + num = 0 + for root, dirs, files in os.walk(path): + for file in files: + num += 1 + return num + + def get_img_list(path): + img_path_list = [] + label_list = os.listdir(path) + for i in range(len(label_list)): + label = label_list[i] + imgs_path = os.path.join(path, label) + imgs_name = os.listdir(imgs_path) + for j in range(len(imgs_name)): + img_name = imgs_name[j] + img_path = os.path.join(imgs_path, img_name) + img_path_list.append(img_path) + return img_path_list + + def data_aug(data_root_path, batch_size = 32): + users = os.listdir(data_root_path) + tags = ["train", "test"] + aug_users = [] + for i in range(len(users)): + user = users[i] + for tag in tags: + data_path = os.path.join(data_root_path, user, tag) + num_data = count_dir(data_path) + if num_data < batch_size: + aug_users.append(user + "_" + tag) + print("user: ", user, " ", tag, " data number: ", num_data, " < ", batch_size, " should be aug") + aug_num = batch_size - num_data + img_path_list = get_img_list(data_path) + for j in range(aug_num): + img_path = choice(img_path_list) + info = img_path.split(".") + aug_img_path = info[0] + "_aug_" + str(j) + ".png" + shutil.copy(img_path, aug_img_path) + print("[aug", j, "]", "============= copy file:", img_path, "to ->", aug_img_path) + print("the number of all aug users: " + str(len(aug_users))) + print("aug user name: ", end=" ") + for k in range(len(aug_users)): + print(aug_users[k], end = " ") + + if __name__ == "__main__": + data_root_path = "leaf/data/femnist/3500_client_img/" + batch_size = 32 + data_aug(data_root_path, batch_size) + ``` + +8. Convert the expanded image dataset into a bin file format usable in the Federated Learning Framework. + + Refer to the following code: + + ```python + import numpy as np + import os + import mindspore.dataset as ds + import mindspore.dataset.vision as vision + import mindspore.dataset.transforms as transforms + import mindspore + + def mkdir(path): + if not os.path.exists(path): + os.mkdir(path) + + def count_id(path): + files = os.listdir(path) + ids = {} + for i in files: + ids[i] = int(i) + return ids + + def create_dataset_from_folder(data_path, img_size, batch_size=32, repeat_size=1, num_parallel_workers=1, shuffle=False): + """ create dataset for train or test + Args: + data_path: Data path + batch_size: The number of data records in each group + repeat_size: The number of replicated data records + num_parallel_workers: The number of parallel workers + """ + # define dataset + ids = count_id(data_path) + mnist_ds = ds.ImageFolderDataset(dataset_dir=data_path, decode=False, class_indexing=ids) + # define operation parameters + resize_height, resize_width = img_size[0], img_size[1] # 32 + + transform = [ + vision.Decode(True), + vision.Grayscale(1), + vision.Resize(size=(resize_height, resize_width)), + vision.Grayscale(3), + vision.ToTensor(), + ] + compose = transforms.Compose(transform) + + # apply map operations on images + mnist_ds = mnist_ds.map(input_columns="label", operations=transforms.TypeCast(mindspore.int32)) + mnist_ds = mnist_ds.map(input_columns="image", operations=compose) + + # apply DatasetOps + buffer_size = 10000 + if shuffle: + mnist_ds = mnist_ds.shuffle(buffer_size=buffer_size) # 10000 as in LeNet train script + mnist_ds = mnist_ds.batch(batch_size, drop_remainder=True) + mnist_ds = mnist_ds.repeat(repeat_size) + return mnist_ds + + def img2bin(root_path, root_save): + """ + transform images to bin files + + Args: + root_path: the root path of 3500 images files + root_save: the root path to save bin files + + """ + + use_list = [] + train_batch_num = [] + test_batch_num = [] + mkdir(root_save) + users = os.listdir(root_path) + for user in users: + use_list.append(user) + user_path = os.path.join(root_path, user) + train_test = os.listdir(user_path) + for tag in train_test: + data_path = os.path.join(user_path, tag) + dataset = create_dataset_from_folder(data_path, (32, 32, 1), 32) + batch_num = 0 + img_list = [] + label_list = [] + for data in dataset.create_dict_iterator(): + batch_x_tensor = data['image'] + batch_y_tensor = data['label'] + trans_img = np.transpose(batch_x_tensor.asnumpy(), [0, 2, 3, 1]) + img_list.append(trans_img) + label_list.append(batch_y_tensor.asnumpy()) + batch_num += 1 + + if tag == "train": + train_batch_num.append(batch_num) + elif tag == "test": + test_batch_num.append(batch_num) + + imgs = np.array(img_list) # (batch_num, 32,3,32,32) + labels = np.array(label_list) + path1 = os.path.join(root_save, user) + mkdir(path1) + image_path = os.path.join(path1, user + "_" + "bn_" + str(batch_num) + "_" + tag + "_data.bin") + label_path = os.path.join(path1, user + "_" + "bn_" + str(batch_num) + "_" + tag + "_label.bin") + + imgs.tofile(image_path) + labels.tofile(label_path) + print("user: " + user + " " + tag + "_batch_num: " + str(batch_num)) + print("total " + str(len(use_list)) + " users finished!") + + root_path = "leaf/data/femnist/3500_client_img/" + root_save = "leaf/data/femnist/3500_clients_bin" + img2bin(root_path, root_save) + ``` + + Print the result as following after running the script, which means a successful run: + + ```sh + user: f0141_43 test_batch_num: 1 + user: f0141_43 train_batch_num: 10 + user: f0137_14 test_batch_num: 1 + user: f0137_14 train_batch_num: 11 + ...... + total 3500 users finished! + ``` + +9. Generate `3500_clients_bin` folder containing a total of 3500 user folders with the following directory structure: + + ```sh + leaf/data/femnist/3500_clients_bin + ├── f0000_14 # User number + │ ├── f0000_14_bn_10_train_data.bin # The training data of user f0000_14 (The number 10 after bn_ represents the batch number) + │ ├── f0000_14_bn_10_train_label.bin # Training tag for user f0000_14 + │ ├── f0000_14_bn_1_test_data.bin # Test data of user f0000_14 (the number 1 after bn_ represents batch number) + │ └── f0000_14_bn_1_test_label.bin # Test tag for user f0000_14 + ├── f0001_41 # User number + │ ├── f0001_41_bn_11_train_data.bin # The training data of user f0001_41 (The number 11 after bn_ represents the batch number) + │ ├── f0001_41_bn_11_train_label.bin # Training tag for user f0001_41 + │ ├── f0001_41_bn_1_test_data.bin # Test data of user f0001_41 (the number 1 after bn_ represents batch number) + │ └── f0001_41_bn_1_test_label.bin # Test tag for user f0001_41 + │ ... + └── f4099_10 # User number + ├── f4099_10_bn_4_train_data.bin # The training data of user f4099_10 (the number 4 after bn_ represents the batch number) + ├── f4099_10_bn_4_train_label.bin # Training tag for user f4099_10 + ├── f4099_10_bn_1_test_data.bin # Test data of user f4099_10 (the number 1 after bn_ represents batch number) + └── f4099_10_bn_1_test_label.bin # Test tag for user f4099_10 + ``` + +The `3500_clients_bin` folder generated according to steps 1 to 9 above can be directly used as the input data for the device-cloud federated image classification task. diff --git a/docs/federated/docs/source_en/index.rst b/docs/federated/docs/source_en/index.rst index 39f7e69a8f..9c76cd68c9 100644 --- a/docs/federated/docs/source_en/index.rst +++ b/docs/federated/docs/source_en/index.rst @@ -108,4 +108,5 @@ Common Application Scenarios :maxdepth: 1 :caption: References + image_classfication_dataset_process faq \ No newline at end of file diff --git a/docs/federated/docs/source_zh_cn/image_classfication_dataset_process.md b/docs/federated/docs/source_zh_cn/image_classfication_dataset_process.md index e7fec758a0..18ff515ef6 100644 --- a/docs/federated/docs/source_zh_cn/image_classfication_dataset_process.md +++ b/docs/federated/docs/source_zh_cn/image_classfication_dataset_process.md @@ -2,7 +2,7 @@ -本教程采用`leaf`数据集中的联邦学习数据集`FEMNIST`, 该数据集包含62个不同类别的手写数字和字母(数字0~9、26个小写字母、26个大写字母),图像大小为`28 x 28`像素,数据集包含3500个用户的手写数字和字母(最多可模拟3500个客户端参与联邦学习),总数据量为805263,平均每个用户包含数据量为226.83,所有用户数据量的方差为88.94。 +本教程采用`leaf`数据集中的联邦学习数据集`FEMNIST`,该数据集包含62个不同类别的手写数字和字母(数字0~9、26个小写字母、26个大写字母),图像大小为`28 x 28`像素,数据集包含3500个用户的手写数字和字母(最多可模拟3500个客户端参与联邦学习),总数据量为805263,平均每个用户包含数据量为226.83,所有用户数据量的方差为88.94。 ## 端云联邦学习图像分类数据集处理 @@ -132,7 +132,7 @@ - `user_name`: 用户名。 - `num_samples`: 用户的样本数。 - - `user_data`: 一个以'x'为key,以用户数据为value的字典对象; 以'y'为key,以用户数据对应的标签为value。 + - `user_data`: 一个以'x'为key,以用户数据为value的字典对象;以'y'为key,以用户数据对应的标签为value。 运行该脚本打印如下,代表运行成功: @@ -434,18 +434,18 @@ ├── f0000_14 # 用户编号 │ ├── f0000_14_bn_10_train_data.bin # 用户f0000_14的训练数据 (bn_后面的数字10代表batch number) │ ├── f0000_14_bn_10_train_label.bin # 用户f0000_14的训练标签 - │ ├── f0000_14_bn_1_test_data.bin # 用户f0000_14的测试数据 (bn_后面的数字1代表batch number) + │ ├── f0000_14_bn_1_test_data.bin # 用户f0000_14的测试数据 (bn_后面的数字1代表batch number) │ └── f0000_14_bn_1_test_label.bin # 用户f0000_14的测试标签 ├── f0001_41 # 用户编号 │ ├── f0001_41_bn_11_train_data.bin # 用户f0001_41的训练数据 (bn_后面的数字11代表batch number) │ ├── f0001_41_bn_11_train_label.bin # 用户f0001_41的训练标签 - │ ├── f0001_41_bn_1_test_data.bin # 用户f0001_41的测试数据 (bn_后面的数字1代表batch number) - │ └── f0001_41_bn_1_test_label.bin # 用户f0001_41的测试标签 + │ ├── f0001_41_bn_1_test_data.bin # 用户f0001_41的测试数据 (bn_后面的数字1代表batch number) + │ └── f0001_41_bn_1_test_label.bin # 用户f0001_41的测试标签 │ ... └── f4099_10 # 用户编号 ├── f4099_10_bn_4_train_data.bin # 用户f4099_10的训练数据 (bn_后面的数字4代表batch number) ├── f4099_10_bn_4_train_label.bin # 用户f4099_10的训练标签 - ├── f4099_10_bn_1_test_data.bin # 用户f4099_10的测试数据 (bn_后面的数字1代表batch number) + ├── f4099_10_bn_1_test_data.bin # 用户f4099_10的测试数据 (bn_后面的数字1代表batch number) └── f4099_10_bn_1_test_label.bin # 用户f4099_10的测试标签 ``` diff --git a/docs/federated/docs/source_zh_cn/image_classification_application_in_cross_silo.md b/docs/federated/docs/source_zh_cn/image_classification_application_in_cross_silo.md index 2fec556f21..a332654cfd 100644 --- a/docs/federated/docs/source_zh_cn/image_classification_application_in_cross_silo.md +++ b/docs/federated/docs/source_zh_cn/image_classification_application_in_cross_silo.md @@ -8,7 +8,7 @@ ## 下载数据集 -本示例采用[leaf数据集](https://github.com/TalwalkarLab/leaf)中的联邦学习数据集`FEMNIST`, 该数据集包含62个不同类别的手写数字和字母(数字0~9、26个小写字母、26个大写字母),图像大小为`28 x 28`像素,数据集包含3500个用户的手写数字和字母(最多可模拟3500个客户端参与联邦学习),总数据量为805263,平均每个用户包含数据量为226.83,所有用户数据量的方差为88.94。 +本示例采用[leaf数据集](https://github.com/TalwalkarLab/leaf)中的联邦学习数据集`FEMNIST`,该数据集包含62个不同类别的手写数字和字母(数字0~9、26个小写字母、26个大写字母),图像大小为`28 x 28`像素,数据集包含3500个用户的手写数字和字母(最多可模拟3500个客户端参与联邦学习),总数据量为805263,平均每个用户包含数据量为226.83,所有用户数据量的方差为88.94。 可参考文档[端云联邦学习图像分类数据集处理](https://www.mindspore.cn/federated/docs/zh-CN/master/image_classfication_dataset_process.html)中步骤1~7获取图片形式的3500个用户数据集`3500_client_img`。 @@ -153,7 +153,7 @@ if __name__ == "__main__": ### 安装MindSpore和Mindspore Federated -包括源码和下载发布版两种方式,支持CPU、GPU硬件平台,根据硬件平台选择安装即可。安装步骤可参考[MindSpore安装指南](https://www.mindspore.cn/install), [Mindspore Federated安装指南](https://www.mindspore.cn/federated/docs/zh-CN/master/federated_install.html)。 +包括源码和下载发布版两种方式,支持CPU、GPU硬件平台,根据硬件平台选择安装即可。安装步骤可参考[MindSpore安装指南](https://www.mindspore.cn/install),[Mindspore Federated安装指南](https://www.mindspore.cn/federated/docs/zh-CN/master/federated_install.html)。 目前联邦学习框架只支持Linux环境中部署,cross-silo联邦学习框架需要MindSpore版本号>=1.5.0。 diff --git a/docs/mindspore/source_en/index.rst b/docs/mindspore/source_en/index.rst index 8cf01bc7b6..9163308ef1 100644 --- a/docs/mindspore/source_en/index.rst +++ b/docs/mindspore/source_en/index.rst @@ -76,7 +76,10 @@ MindSpore Documentation migration_guide/overview migration_guide/enveriment_preparation + migration_guide/analysis_and_preparation migration_guide/model_development/model_development + migration_guide/debug_and_tune + migration_guide/sample_code migration_guide/use_third_party_op .. toctree:: diff --git a/docs/mindspore/source_en/migration_guide/analysis_and_preparation.md b/docs/mindspore/source_en/migration_guide/analysis_and_preparation.md new file mode 100644 index 0000000000..2925e33cce --- /dev/null +++ b/docs/mindspore/source_en/migration_guide/analysis_and_preparation.md @@ -0,0 +1,475 @@ +# Model Analysis and Preparation + + + +## Obtaining Sample Code + +When you obtain a paper to implement migration on MindSpore, you need to find the reference code that has been implemented in other frameworks. In principle, the reference code must meet at least one of the following requirements: + +1. The author opens the paper to the public. +2. The implementation is starred and forked by many developers, which means it is widely recognized. +3. The code is new and maintained by developers. +4. The PyTorch reference code is preferred. + +If a new paper has no reference implementation, you can refer to [Constructing MindSpore Network](https://www.mindspore.cn/docs/en/master/migration_guide/model_development/model_development.html). + +## Analyzing Algorithm and Network Structure + +First, when reading the paper and reference code, you need to analyze the network structure to organize the code writing. The following shows the general network structure of YOLOX. + +| Module| Implementation| +| ---- | ---- | +| backbone | CSPDarknet (s, m, l, x)| +| neck | FPN | +| head | Decoupled Head | + +Second, analyze the innovative points of the migration algorithm and record the tricks used during the training, for example, data augmentation added during data processing, shuffle, optimizer, learning rate attenuation policy, and parameter initialization. You can prepare a checklist and fill in the corresponding items during analysis. + +For example, the following records some tricks used by the YOLOX network during training. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TrickRecord
Data augmentationMosaic, including random scaling, crop, and layout
MixUp
Learning rate attenuation policyMultiple attenuation modes are available. By default, the COS learning rate attenuation is used.
Optimizer parametersSGD momentum=0.9, nesterov=True, and no weight decay
Training parametersepoch: 300; batchsize: 8
Network structure optimization pointsDecoupled Head; Anchor Free; SimOTA
Training process optimization points EMA; Data augmentation is not performed for the last 15 epochs; mixed precision
+ +**Note that the tricks used in the code are mainly reproduced. The tricks mentioned in some papers may not be useful.** + +In addition, you need to determine whether the paper can be implemented by modifying the existing MindSpore model. If yes, you can greatly reduce the development workload. For example, WGAN-PG can be developed based on WGAN. +[MindSpore models](https://gitee.com/mindspore/models) is a model repository. It covers mainstream models in multiple fields, such as machine vision, natural language processing, voice, and recommendation system. You can check whether there are required models from the repository. + +## Reproducing Paper Implementation + +After obtaining the reference code, you need to reproduce the accuracy of the reference implementation and obtain the performance data of the reference implementation. This has the following advantages: + +1. Identify some issues in advance. + + - Check whether the third-party repository used by the reference code depends on a version to identify version adaptation problems in advance. + - Check whether the dataset can be obtained. Some datasets are private or the author adds some datasets to the public dataset. This problem can be found at the reproduction reference implementation stage. + - Check whether the reference implementation can reproduce the accuracy of the paper. Some official reference implementations may not reproduce the accuracy of the paper. In this case, detect the problem in time, replace the reference implementation, or adjust the accuracy baseline. + +3. Obtain some reference data for the MindSpore migration process. + + - Obtain the loss decrease trend to check whether the training convergence trend on MindSpore is normal. + - Obtain the parameter file for conversion and inference verification. For details, see [Inference and Training Process](https://www.mindspore.cn/docs/en/master/migration_guide/model_development/training_and_evaluation_procession.html). + - Obtain the performance baseline for performance tuning. For details, see [Debugging and Tuning](https://www.mindspore.cn/docs/en/master/migration_guide/debug_and_tune.html). + +## Analyzing API Compliance + +The API missing analysis here refers to APIs in the network execution diagram, including MindSpore [operators](https://www.mindspore.cn/docs/en/master/api_python/mindspore.ops.html) and advanced encapsulated APIs, and excluding the APIs used in data processing. You are advised to use third-party APIs, such as NumPy, OpenCV, Pandas, and PIL, to replace APIs used in data processing. + +### Querying the API Mapping Table + +Take the PyTorch code migration as an example. After obtaining the reference code implementation, you can filter keywords such as `torch`, `nn`, and `ops` to obtain the used APIs. If the method of another repository is invoked, you need to manually analyze the API. Then, check the [PyTorch and MindSpore API Mapping Table](https://www.mindspore.cn/docs/en/master/note/api_mapping/pytorch_api_mapping.html). +Alternatively, the [API](https://www.mindspore.cn/docs/en/master/api_python/mindspore.ops.html) searches for the corresponding API implementation. + +For details about the mapping of other framework APIs, see the [API naming and function description](https://www.mindspore.cn/docs/en/master/api_python/mindspore.html). For APIs with the same function, the names of MindSpore may be different from those of other frameworks. The parameters and functions of APIs with the same name may also be different from those of other frameworks. For details, see the official description. + +If the corresponding API is not found, see specific missing API processing policy. + +### Missing API Processing Policy + +You can use the following methods to process the missing API: + +#### 1. Use equivalent replacement + +In some scenarios, API functions can be equivalently replaced. For example: + +- As Squeeze, Flatten, and ExpandDims do not perform actual calculation, APIs with only Tensor shape changed can be replaced by Reshape. + +- When the output shape of AdaptiveAvgPool and AdaptiveMaxPool is 1, AdaptiveAvgPool and AdaptiveMaxPool are equivalent to ReduceMean and ReduceMax when `keep_dims` is set to `True`. + +- MaxPool and MaxPoolWithArgmax are equivalent when indices are not used. + +- Sort is equivalent to TopK in the full sorting scenario. + +#### 2. Use existing APIs to package equivalent function logic + +For some missing APIs, equivalent functions can be implemented based on existing MindSpore APIs. The following is an example of `sigmoid focal loss`: + +First, let's analyze the algorithm basis of the API. + +Focal Loss[1] is a method used to deal with the imbalance of positive and negative references and difficult references during the training of a single-phase target detector. + +Generally, the sigmoid focal loss API is implemented by MMDetection. The following shows how PyTorch implements this API. + +```python +import torch.nn.functional as F + +def reduce_loss(loss, reduction): + """Reduce loss as specified. + Args: + loss (Tensor): Elementwise loss tensor. + reduction (str): Options are "none", "mean" and "sum". + Return: + Tensor: Reduced loss tensor. + """ + reduction_enum = F._Reduction.get_enum(reduction) + # none: 0, elementwise_mean:1, sum: 2 + if reduction_enum == 0: + return loss + elif reduction_enum == 1: + return loss.mean() + elif reduction_enum == 2: + return loss.sum() + +def weight_reduce_loss(loss, weight=None, reduction='mean', avg_factor=None): + if weight is not None: + loss = loss * weight + + # if avg_factor is not specified, just reduce the loss + if avg_factor is None: + loss = reduce_loss(loss, reduction) + else: + # if reduction is mean, then average the loss by avg_factor + if reduction == 'mean': + loss = loss.sum() / avg_factor + # if reduction is 'none', then do nothing, otherwise raise an error + elif reduction != 'none': + raise ValueError('avg_factor can not be used with reduction="sum"') + return loss + + +def py_sigmoid_focal_loss(pred, + target, + weight=None, + gamma=2.0, + alpha=0.25, + reduction='mean', + avg_factor=None): + """PyTorch version of `Focal Loss `_. + Args: + pred (torch.Tensor): The prediction with shape (N, C), C is the + number of classes + target (torch.Tensor): The learning label of the prediction. + weight (torch.Tensor, optional): Sample-wise loss weight. + gamma (float, optional): The gamma for calculating the modulating + factor. Defaults to 2.0. + alpha (float, optional): A balanced form for Focal Loss. + Defaults to 0.25. + reduction (str, optional): The method used to reduce the loss into + a scalar. Defaults to 'mean'. + avg_factor (int, optional): Average factor that is used to average + the loss. Defaults to None. + """ + pred_sigmoid = pred.sigmoid() + target = target.type_as(pred) + pt = (1 - pred_sigmoid) * target + pred_sigmoid * (1 - target) + focal_weight = (alpha * target + (1 - alpha) * + (1 - target)) * pt.pow(gamma) + loss = F.binary_cross_entropy_with_logits( + pred, target, reduction='none') * focal_weight + if weight is not None: + if weight.shape != loss.shape: + if weight.size(0) == loss.size(0): + # For most cases, weight is of shape (num_priors, ), + # which means it does not have the second axis num_class + weight = weight.view(-1, 1) + else: + # Sometimes, weight per anchor per class is also needed. e.g. + # in FSAF. But it may be flattened of shape + # (num_priors x num_class, ), while loss is still of shape + # (num_priors, num_class). + assert weight.numel() == loss.numel() + weight = weight.view(loss.size(0), -1) + assert weight.ndim == loss.ndim + loss = weight_reduce_loss(loss, weight, reduction, avg_factor) + return loss +``` + +According to the API mapping table, the APIs used in the code have corresponding implementations on MindSpore. + +Implement the MindSpore version by referring to the preceding PyTorch code. + +```python +import mindspore as ms +from mindspore import nn, ops + +class SigmoidFoaclLoss(nn.Cell): + def __init__(self, weight=None, gamma=2.0, alpha=0.25, reduction='mean', avg_factor=None): + super(SigmoidFoaclLoss, self).__init__() + self.sigmoid = ops.Sigmoid() + self.alpha = alpha + self.gamma = gamma + self.weight = ms.Tensor(weight) if weight is not None else weight + self.reduction = reduction + self.avg_factor = avg_factor + self.binary_cross_entropy_with_logits = nn.BCEWithLogitsLoss(reduction="none") + self.is_weight = (weight is not None) + + def reduce_loss(self, loss): + """Reduce loss as specified. + Args: + loss (Tensor): Elementwise loss tensor. + Return: + Tensor: Reduced loss tensor. + """ + if self.reduction == "mean": + return loss.mean() + elif self.reduction == "sum": + return loss.sum() + return loss + + def weight_reduce_loss(self, loss): + # if avg_factor is not specified, just reduce the loss + if self.avg_factor is None: + loss = self.reduce_loss(loss) + else: + # if reduction is mean, then average the loss by avg_factor + if self.reduction == 'mean': + loss = loss.sum() / self.avg_factor + # if reduction is 'none', then do nothing, otherwise raise an error + elif self.reduction != 'none': + raise ValueError('avg_factor can not be used with reduction="sum"') + return loss + + def construct(self, pred, target): + pred_sigmoid = self.sigmoid(pred) + target = ops.cast(target, pred.dtype) + pt = (1 - pred_sigmoid) * target + pred_sigmoid * (1 - target) + focal_weight = (self.alpha * target + (1 - self.alpha) * (1 - target)) * ops.pow(pt, self.gamma) + loss = self.binary_cross_entropy_with_logits(pred, target) * focal_weight + if self.is_weight: + weight = self.weight + if self.weight.shape != loss.shape: + if self.weight.shape[0] == loss.shape[0]: + # For most cases, weight is of shape (num_priors, ), + # which means it does not have the second axis num_class + weight = self.weight.view(-1, 1) + elif self.weight.size == loss.size: + # Sometimes, weight per anchor per class is also needed. e.g. + # in FSAF. But it may be flattened of shape + # (num_priors x num_class, ), while loss is still of shape + # (num_priors, num_class). + weight = self.weight.view(loss.shape[0], -1) + elif self.weight.ndim != loss.ndim: + raise ValueError(f"weight shape {self.weight.shape} is not match to loss shape {loss.shape}") + loss = loss * weight + loss = self.weight_reduce_loss(loss) + return loss +``` + +Then, perform a test. + +```python +import torch +import numpy as np +np.random.seed(1) + +def test_compare(pred, target, weight, gamma=2.0, alpha=0.25, reduction='mean', avg_factor=None): + ms_s_focal_loss = SigmoidFoaclLoss(weight=weight, gamma=gamma, alpha=alpha, + reduction=reduction, avg_factor=avg_factor) + loss_ms = ms_s_focal_loss(ms.Tensor(pred), ms.Tensor(target)) + loss_pt = py_sigmoid_focal_loss(torch.from_numpy(pred), torch.from_numpy(target), weight=torch.from_numpy(weight), + gamma=gamma, alpha=alpha, reduction=reduction, avg_factor=avg_factor) + print(np.max(np.abs(loss_ms.asnumpy() - loss_pt.numpy()))) + +pred = np.random.uniform(-1, 1, (3, 4)).astype(np.float32) +target = np.random.uniform(-1, 1, (3, 4)).astype(np.float32) +weight = np.random.uniform(0, 1, (3,)).astype(np.float32) + +test_compare(pred, target, weight, gamma=2.0, alpha=0.25, reduction='mean', avg_factor=None) +test_compare(pred, target, weight, gamma=1.0, alpha=0.5, reduction='sum', avg_factor=None) +test_compare(pred, target, weight, gamma=2.0, alpha=0.25, reduction='mean', avg_factor=0.3) +test_compare(pred, target, weight, gamma=2.0, alpha=0.25, reduction='none', avg_factor=None) +``` + +The final error is less than 1e-5, which is a reasonable accuracy error. + +```text +6.891787e-08 +1.4305115e-06 +2.8014183e-06 +3.799796e-07 +``` + +#### 3. Customize operators + +When existing APIs cannot be used for packaging, or the performance of cell encapsulation is poor, you need to customize operators. For details, see [Custom Operators](https://www.mindspore.cn/tutorials/experts/en/master/operation/op_custom.html). + +In addition to migrating APIs, you can also use the `aot` development mode of the `Custom` operator to call the PyTorch Aten operator for quick verification. For details, see [Using Third-party Operator Libraries Based on Customized Interfaces](https://www.mindspore.cn/docs/en/master/migration_guide/use_third_party_op.html). + +**Note that it is convenient to migrate operators implemented by PyTorch to the GPU and CPU. Most of the operators displayed here are GPU and CPU operators. Ascend operators need to use the TBE for operator development, which has high requirements. Therefore, you are advised to use officially implemented operators for packaging.** + +#### 4. Seek help from the community + +Commit an issue on [MindSpore Gitee](https://gitee.com/mindspore/mindspore/issues) to suggest developing missing APIs. + +## Analyzing Function Compliance + +During continuous delivery of MindSpore, some functions are restricted. If restricted functions are involved during network migration, some measures can be taken to avoid the impact of function restrictions. + +### Dynamic shape + +To know dynamic shape, you need to know what is a static shape. +Static shape indicates that the shape of a tensor does not change during network execution. +For example, on the ResNet50 network, if the input shape of an image is always `224*224`, the shapes of the output Tesnor of the four residual modules are `B*64*56*56`, `B*128*28*28`, `B*256*14*14`, and `B*512*7*7` respectively in the network training phase. `B` indicates `BatchSize`, which is also fixed during the training. In this case, all shapes on the network are static and no dynamic shape is available. +If the input shape may no+t be `224*224`, the shape of the output tensor of the four residual modules varies with the input shape. In this case, the shape is dynamic instead of static. Generally, dynamic shape is introduced due to the following reasons: + +#### Input shape not fixed + +For example, the input image has different shapes, and the audio label has different lengths. In this case, dynamic shapes are introduced. + +In this scenario, you can read the code to check whether the output shape of data processing is fixed, or directly print the output shape of data processing for comparison. + +```python +for batch_idx, (data, target) in enumerate(data_loader): + print(batch_idx, data.shape, target.shape) + print("="*20) +``` + +#### APIs that cause shape changes during network execution + +During network execution, some operations may cause tensor shape changes. + +The common APIs that cause this scenario are as follows: + +| API | Description| Dynamic Shape Scenario| +| ---- | ----- | ------- | +| StridedSlice/Slice | Specifies a slice. You can also use [start_idx:end_idx] during programming.| The slice subscript is a variable.| +| TopK | Obtains the first K data.| The value of K is not fixed.| +| Gather | Obtains the slice consisting of the elements corresponding to the tensor index on the specified axis.| The index length is not fixed.| +| UnsortedSegmentX | Specifies computation of an input tensor, including UnsortedSegmentSum and UnsortedSegmentMax.| The segment is not fixed.| +| Sampler | Specifies sampler-related operations, such as where and random.choice.| The sample quantity is not fixed.| +| ReduceX | Specifies a reduction operation, such as ReduceSum and ReduceMean.| The axis is not fixed.| +| Transpose | Performs transformation based on the axis.| The axis is not fixed.| +| Unique | Deduplicates data.| Dynamic shape is introduced when this API is used.| +| MaskedSelect | Obtains the value of mask based on the Boolean type.| Dynamic shape is introduced when this API is used.| + +For example: + +```python +import numpy as np +import mindspore as ms +np.random.seed(1) +x = ms.Tensor(np.random.uniform(0, 1, (10)).astype(np.float32)) +k = ms.Tensor(np.random.randint(1, 10), ms.int64) +print(k) +print(x[:k].shape) +# 6 +# (6,) +``` + +During network training, there is a slicing operation `x[:k]`. Here, k is not a constant. As a result, the shape of `x[:k]` changes with the value of k, and the shape of all subsequent operations related to `x[:k]` is uncertain. + +#### Shape changes introduced by different branches of control flows + +The output of some control flows on the network may be different. When the condition control items of the control flows are not fixed, dynamic shape may be triggered. For example: + +```python +import numpy as np +import mindspore as ms +from mindspore import ops +np.random.seed(1) +x = ms.Tensor(np.random.uniform(0, 1, (10)).astype(np.float32)) +cond = (x > 0.5).any() + +if cond: + y = ops.masked_select(x, x > 0.5) +else: + y = ops.zeros_like(x) +print(x) +print(cond) +print(y) + +# [4.17021990e-01 7.20324516e-01 1.14374816e-04 3.02332580e-01 +# 1.46755889e-01 9.23385918e-02 1.86260208e-01 3.45560730e-01 +# 3.96767467e-01 5.38816750e-01] +# True +# [0.7203245 0.53881675] +``` + +In this process, there are two dynamic shapes. One is that the shape of the `masked_select` result is dynamic if `cond=True`. The other is the control flow. Because `cond` is uncertain, the shape output of the two branches of the control flow is different, which also causes the dynamic shape. + +Generally, the dynamic shape can be analyzed at the algorithm and code layers, or the tensor related to the reference code can be directly printed for judgment. If dynamic shape exists, we will introduce the workaround in [Network Body and Loss Setup](https://www.mindspore.cn/docs/en/master/migration_guide/model_development/model_and_loss.html). + +#### Sparsity + +A [sparse tensor](https://matteding.github.io/2019/04/25/sparse-matrices/) is a special tensor in which the value of the most significant element is zero. + +In some scenarios (such as recommendation systems, molecular dynamics, graph neural networks), the data is sparse. If you use common dense tensors to represent the data, you may introduce many unnecessary calculations, storage, and communication costs. In this case, it is better to use sparse tensor to represent the data. + +MindSpore now supports the most commonly used [CSR and COO data formats](https://www.mindspore.cn/tutorials/en/master/beginner/tensor.html#sparse-tensor). Currently, only a limited number of sparse operators are supported, and most sparse features are restricted. In this case, you are advised to check whether the corresponding operator supports sparse computing. If the operator does not support sparse computing, convert it into a common operator. +After the operator is converted into a dense operator, the video memory used increases. Therefore, the batch size implemented by referring to may not be used for training. In this case, you can use [Gradient Accumulation](https://www.mindspore.cn/tutorials/experts/en/master/others/gradient_accumulation.html) to simulate large batch training. + +## MindSpore Function/Feature Recommendation + +### [Dynamic and Static Graphs](https://www.mindspore.cn/tutorials/en/master/advanced/compute_graph.html) + +Currently, there are two execution modes of a mainstream deep learning framework: a static graph mode (Graph) and a dynamic graph mode (PyNative). + +- In static graph mode, when the program is built and executed, the graph structure of the neural network is generated first, and then the computation operations involved in the graph are performed. Therefore, in static graph mode, the compiler can achieve better execution performance by using technologies such as graph optimization, which facilitates large-scale deployment and cross-platform running. + +- In dynamic graph mode, the program is executed line by line according to the code writing sequence. In the forward execution process, the backward execution graph is dynamically generated according to the backward propagation principle. In this mode, the compiler delivers the operators in the neural network to the device one by one for computing, facilitating users to build and debug the neural network model. + +### [Calling the Custom Class](https://www.mindspore.cn/tutorials/experts/en/master/network/ms_class.html) + +In static graph mode, you can use `ms_class` to modify a custom class. You can create and call an instance of the custom class, and obtain its attributes and methods. + +`ms_class` is applied to the static graph mode to expand the support scope of static graph compilation syntax. In dynamic graph mode, that is, PyNative mode, the use of `ms_class` does not affect the execution logic of PyNative mode. + +### [Automatic Differential](https://www.mindspore.cn/tutorials/en/master/beginner/autograd.html) + +Automatic differentiation can calculate a derivative value of a derivative function at a certain point, which is a generalization of backward propagation algorithms. The main problem solved by automatic differential is to decompose a complex mathematical operation into a series of simple basic operations. This function shields a large number of derivative details and processes from users, greatly reducing the threshold for using the framework. + +### [Mixed Precision](https://www.mindspore.cn/tutorials/experts/en/master/others/mixed_precision.html) + +Generally, when a neural network model is trained, the default data type is FP32. In recent years, to accelerate training time, reduce memory occupied during network training, and store a trained model with same precision, more and more mixed-precision training methods are proposed in the industry. The mixed-precision training herein means that both single precision (FP32) and half precision (FP16) are used in a training process. + +### [Auto Augmentation](https://www.mindspore.cn/tutorials/experts/en/master/dataset/augment.html) + +MindSpore not only allows you to customize data augmentation, but also provides an automatic data augmentation mode to automatically perform data augmentation on images based on specific policies. + +### [Multi Dimensional](https://www.mindspore.cn/tutorials/experts/en/master/parallel/multi_dimensional.html) + +With the development of deep learning, the model scale becomes larger and larger. For example, in the NLP field, the number of parameters has increased from 100 million in BERT to 170 billion in GPT-3, and then to 200 billion in PanGu-Alpha. Currently, the industry even proposes millions of parameters. It can be seen that the parameter scale has an exponential growth trend in recent years. On the other hand, with the development of technologies in fields such as big data and the Internet, datasets that can be used for model training also increase rapidly. For example, the size of datasets in scenarios such as recommendation and natural language processing can reach TB level. + +### [Gradient Accumulation Algorithm](https://www.mindspore.cn/tutorials/experts/en/master/others/gradient_accumulation.html) + +Gradient accumulation is a method of splitting data samples for training neural networks into several small batches by batch and then calculating the batches in sequence. The purpose is to solve the out of memory (OOM) problem that the neural network cannot be trained or the network model cannot be loaded due to insufficient memory. + +### [Summary](https://www.mindspore.cn/mindinsight/docs/en/master/summary_record.html) + +Scalars, images, computational graphs, training optimization processes, and model hyperparameters during training are recorded in files and can be viewed on the web page. + +### [Debugger](https://www.mindspore.cn/mindinsight/docs/en/master/debugger.html) + +The MindSpore debugger is a debugging tool provided for graph mode training. It can be used to view and analyze the intermediate results of graph nodes. + +### [Golden Stick](https://www.mindspore.cn/golden_stick/docs/en/master/index.html) + +MindSpore Golden Stick is a model compression algorithm set jointly designed and developed by Huawei Noah's team and Huawei MindSpore team. It contains basic quantization and pruning methods. + +## Differences Between MindSpore and PyTorch APIs + +When migrating the network from PyTorch to MindSpore, pay attention to the differences between MindSpore and [typical PyTorch APIs](https://www.mindspore.cn/docs/en/master/migration_guide/typical_api_comparision.html). + +[1] Lin, T. Y. , et al. "Focal Loss for Dense Object Detection." IEEE Transactions on Pattern Analysis & Machine Intelligence PP.99(2017):2999-3007. diff --git a/docs/mindspore/source_en/migration_guide/debug_and_tune.md b/docs/mindspore/source_en/migration_guide/debug_and_tune.md new file mode 100644 index 0000000000..83ee43e87a --- /dev/null +++ b/docs/mindspore/source_en/migration_guide/debug_and_tune.md @@ -0,0 +1,296 @@ +# Debugging and Tuning + + + +## Function Debugging + +During network migration, you are advised to use the PyNative mode for debugging. In PyNative mode, you can perform debugging, and log printing is user-friendly. After the debugging is complete, the graph mode is used. The graph mode is more user-friendly in execution performance. You can also find some problems in network compilation. For example, gradient truncation caused by third-party operators. +For details, see [Function Debugging](https://www.mindspore.cn/tutorials/experts/en/master/debug/function_debug.html). + +## Accuracy Debugging + +The accuracy debugging process is as follows: + +### 1. Checking Parameters + +This part includes checking all parameters and the number of trainable parameters, and checking the shape of all parameters. + +#### Obtaining MindSpore Parameters + +`Parameter` is used for MindSpore trainable and untrainable parameters. + +```python +from mindspore import nn + +class msNet(nn.Cell): + def __init__(self): + super(msNet, self).__init__() + self.fc = nn.Dense(1, 1, weight_init='normal') + def construct(self, x): + output = self.fc(x) + return output + +msnet = msNet() +# Obtain all parameters. +all_parameter = [] +for item in msnet.get_parameters(): + all_parameter.append(item) + print(item.name, item.data.shape) +print(f"all parameter numbers: {len(all_parameter)}") + +# Obtain trainable parameters. +trainable_params = msnet.trainable_params() +for item in trainable_params: + print(item.name, item.data.shape) +print(f"trainable parameter numbers: {len(trainable_params)}") +``` + +```text + fc.weight (1, 1) + fc.bias (1,) + all parameter numbers: 2 + fc.weight (1, 1) + fc.bias (1,) + trainable parameter numbers: 2 +``` + +#### Obtaining PyTorch Parameters + +`Parameter` is used for PyTorch trainable parameters, and `requires_grad=False` or `buffer` is used for PyTorch untrainable parameters. + +```python +from torch import nn + +class ptNet(nn.Module): + def __init__(self): + super(ptNet, self).__init__() + self.fc = nn.Linear(1, 1) + def construct(self, x): + output = self.fc(x) + return output + + +ptnet = ptNet() +all_parameter = [] +trainable_params = [] +# Obtain network parameters. +for name, item in ptnet.named_parameters(): + if item.requires_grad: + trainable_params.append(item) + all_parameter.append(item) + print(name, item.shape) + +for name, buffer in ptnet.named_buffers(): + all_parameter.append(buffer) + print(name, buffer.shape) +print(f"all parameter numbers: {len(all_parameter)}") +print(f"trainable parameter numbers: {len(trainable_params)}") +``` + +```text + fc.weight torch.Size([1, 1]) + fc.bias torch.Size([1]) + all parameter numbers: 2 + trainable parameter numbers: 2 +``` + +The parameters of MindSpore and PyTorch are similar except BatchNorm. Note that MindSpore does not have parameters corresponding to `num_batches_tracked`. You can replace this parameter with `global_step` in the optimizer. + +| MindSpore | PyTorch | +| --------- | --------| +| gamma | weight | +| beta | bias | +| moving_mean | running_mean | +| moving_variance | running_var | +| -| num_batches_tracked | + +### 2. Model Verification + +The implementation of the model algorithm is irrelevant to the framework. The trained parameters can be converted into the [checkpoint](https://www.mindspore.cn/tutorials/en/master/beginner/save_load.html) file of MindSpore and loaded to the network for inference verification. + +For details about the model verification process, see [ResNet Network Migration](https://www.mindspore.cn/docs/en/master/migration_guide/sample_code.html#model-validation). + +### 3. Inference Verification + +After confirming that the model structures are the same, you are advised to perform inference verification again. In addition to models, the entire inference process also involves datasets and metrics. When the inference results are inconsistent, you can use the control variable method to gradually rectify the fault. + +For details about the inference verification process, see [ResNet Network Migration](https://www.mindspore.cn/docs/en/master/migration_guide/sample_code.html#inference-process). + +### 4. Training Accuracy + +After the inference verification is complete, the basic model, data processing, and metrics calculation are normal. If the training accuracy is still abnormal, how do we locate the fault? + +- Add loss scale. On Ascend, operators such as Conv, Sort, and TopK can only be float16. MatMul is recommended to be float16 due to performance problems. Therefore, it is recommended that loss scale be used as a standard configuration for network training. + +```python +import mindspore as ms +from mindspore import nn +# Model +loss_scale_manager = ms.FixedLossScaleManager(drop_overflow_update=False) # Static loss scale +# loss_scale_manager = ms.DynamicLossScaleManager() # Dynamic loss scale + +# 1. General process +loss = nn.MSELoss() +opt = nn.Adam(params=msnet.trainable_params(), learning_rate=0.01) +model = ms.Model(network=msnet, loss_fn=loss, optimizer=opt, loss_scale_manager=loss_scale_manager) + +# 2. Self-packaged forward network and loss function +msnet.to_float(ms.float16) +loss.to_float(ms.float32) +net_with_loss = nn.WithLossCell(msnet, loss) +# It is recommended that loss_fn be used for the mixed precision of the model. Otherwise, float16 is used for calculation of the loss part, which may cause overflow. +model = ms.Model(network=net_with_loss, optimizer=opt) + +# 3. Self-packaged training process +scale_sense = nn.FixedLossScaleUpdateCell(1)#(config.loss_scale) # Static loss scale +# scale_sense = nn.DynamicLossScaleUpdateCell(loss_scale_value=config.loss_scale, +# scale_factor=2, scale_window=1000) # Dynamic loss scale +train_net = nn.TrainOneStepWithLossScaleCell(net_with_loss, optimizer=opt, scale_sense=scale_sense) +model = ms.Model(network=train_net) +``` + +- Check whether overflow occurs. When loss scale is added, overflow detection is added by default to monitor the overflow result. If overflow occurs continuously, you are advised to use the [debugger](https://www.mindspore.cn/mindinsight/docs/en/master/debugger.html) or [dump data](https://mindspore.cn/tutorials/experts/en/master/debug/dump.html) of MindInsight to check why overflow occurs. + +```python +import numpy as np +from mindspore import dataset as ds + +def get_data(num, w=2.0, b=3.0): + for _ in range(num): + x = np.random.uniform(-10.0, 10.0) + noise = np.random.normal(0, 1) + y = x * w + b + noise + yield np.array([x]).astype(np.float32), np.array([y]).astype(np.float32) + + +def create_dataset(num_data, batch_size=16, repeat_size=1): + input_data = ds.GeneratorDataset(list(get_data(num_data)), column_names=['data', 'label']) + input_data = input_data.batch(batch_size, drop_remainder=True) + input_data = input_data.repeat(repeat_size) + return input_data + +train_net.set_train() +dataset = create_dataset(1600) +iterator = dataset.create_tuple_iterator() +for i, data in enumerate(iterator): + loss, overflow, scaling_sens = train_net(*data) + print("step: {}, loss: {}, overflow:{}, scale:{}".format(i, loss, overflow, scaling_sens)) +``` + +```text + step: 0, loss: 138.42825, overflow:False, scale:1.0 + step: 1, loss: 118.172104, overflow:False, scale:1.0 + step: 2, loss: 159.14542, overflow:False, scale:1.0 + step: 3, loss: 150.65671, overflow:False, scale:1.0 + ... ... + step: 97, loss: 69.513245, overflow:False, scale:1.0 + step: 98, loss: 51.903114, overflow:False, scale:1.0 + step: 99, loss: 42.250656, overflow:False, scale:1.0 +``` + +- Check the optimizer, loss, and parameter initialization. In addition to the model and dataset, only the optimizer, loss, and parameter initialization are added in the entire training process. If the training is abnormal, check the optimizer, loss, and parameter initialization. Especially for loss and parameter initialization, there is a high probability that the problem occurs. +- Check whether to add seeds for multiple devices to ensure that the initialization of multiple SIM cards is consistent. Determine whether to perform gradient aggregation during [customized training](https://www.mindspore.cn/docs/en/master/migration_guide/model_development/training_and_gradient.html#customizing-training-cell). + +```python +import mindspore as ms +ms.set_seed(1) # The random seeds of MindSpore, NumPy, and dataset are fixed. The random seed of the API needs to be set in the API attribute. +``` + +- Check whether the data processing meets the expectation through visualization. Focus on data shuffle and check whether data mismatch occurs. + +For details about more accuracy debugging policies, see [Accuracy Debugging](https://mindspore.cn/mindinsight/docs/en/master/accuracy_problem_preliminary_location.html). + +## Performance Tuning + +The performance tuning directions are as follows: + +1. Operator performance tuning +2. Framework enabling performance tuning +3. Multi-Node synchronization performance tuning +4. Data processing performance tuning + +For details, see [ResNet Network Migration](https://www.mindspore.cn/docs/en/master/migration_guide/sample_code.html). + +> Some networks are large or there are many [process control statements](https://mindspore.cn/tutorials/en/master/advanced/modules/control_flow.html). In this case, the build is slow in graph mode. During performance tuning, distinguish graph build from network execution. This section describes the performance tuning policies in the network execution phase. If graph build is slow, try [incremental operator build](https://mindspore.cn/tutorials/experts/en/master/debug/op_compilation.html) or contact [MindSpore community](https://gitee.com/mindspore/mindspore/issues) for feedback. + +### Operator Performance Tuning + +#### Poor Operator Performance + +If a single operator takes a long time and the performance of the same operator varies greatly in different shapes or data types, the problem is caused by the operator performance. The solution is as follows: + +1. Use data types with less computational workload. For example, if there is no obvious difference between the precision of the same operator in float16 and float32 modes, you can use the float16 format with less calculation workload. +2. Use other operators with the same algorithm to avoid this problem. +3. Pay attention to 16-alignment in the Ascend environment. Due to the design of the Ascend AI Processors, it is recommended that the calculation on the AI core be 16-alignment (each dimension in the shape is a multiple of 16). +4. [Operator Tuning](https://mindspore.cn/tutorials/experts/en/master/debug/auto_tune.html). + +If you find an operator with poor performance, you are advised to contact [MindSpore community](https://gitee.com/mindspore/mindspore/issues) for feedback. We will optimize the operator in time after confirming that the problem is caused by poor performance. + +### Framework Enabling Performance Tuning + +#### Using the Static Graph Mode + +Generally, MindSpore in static graph mode is much faster than that in PyNative mode. It is recommended that training and inference be performed in static graph mode. For details, see [Combination of Dynamic and Static Graphs](https://www.mindspore.cn/docs/en/master/design/dynamic_graph_and_static_graph.html). + +#### On-device Execution + +MindSpore provides an [on-device execution method](https://www.mindspore.cn/docs/en/master/design/overview.html) to concurrently process data and execute the network on the device. You only need to set `dataset_sink_mode=True` in `model.train`. Note that this configuration is `True` by default. When this configuration is enabled, one epoch returns the result of only one network. You are advised to change the value to `False` during debugging. + +#### Using Automatic Mixed Precision + +The mixed precision training method accelerates the deep neural network training process by mixing the single-precision floating-point data format and the half-precision floating-point data format without compromising the network accuracy. Mixed precision training can accelerate the computing process, reduce memory usage and retrieval, and enable a larger model or batch size to be trained on specific hardware. + +For details, see [Mixed Precision Tutorial](https://www.mindspore.cn/tutorials/experts/en/master/others/mixed_precision.html). + +#### Enabling Graph Kernel Fusion + +Graph kernel fusion is a unique network performance optimization technology of MindSpore. It can automatically analyze and optimize the logic of existing network computational graphs, simplify and replace computational graphs, split and fuse operators, and build operators in a special way based on the target hardware capability to improve the computing resource utilization of devices and optimize the overall network performance. Compared with traditional optimization technologies, the graph kernel fusion technology has unique advantages, such as joint optimization of multiple operators across boundaries, cross-layer collaboration with operator compilation, and real-time compilation of operators based on Polyhedral. In addition, the entire optimization process of graph kernel fusion can be automatically completed after users enable the corresponding configuration. Network developers do not need to perform extra perception, so that users can focus on network algorithm implementation. + +Graph kernel fusion applies to scenarios that have high requirements on network execution time. Basic operators are combined to implement customized combination operators and these basic operators are automatically fused to improve the performance of the customized combination operators. + +For details, see [Graph Kernel Fusion Tutorial](https://www.mindspore.cn/docs/en/master/design/graph_fusion_engine.html). + +#### Others + +If there are too many conversion operators (TransData and Cast operators) and the conversion takes a long time, analyze the necessity of the manually added Cast operator. If the accuracy is not affected, delete the redundant Cast and TransData operators. + +If there are too many conversion operators automatically generated by MindSpore, the MindSpore framework may not be fully optimized for some special cases. In this case, contact [MindSpore community](https://gitee.com/mindspore/mindspore/issues) for feedback. + +In [dynamic shape scenario](https://www.mindspore.cn/docs/en/master/migration_guide/analysis_and_preparation.html), continuous graph build is required, which may cause a long end-to-end training time. You are advised to [avoid dynamic shape](https://www.mindspore.cn/docs/en/master/migration_guide/model_development/model_and_loss.html). + +### Multi-Node Synchronization Performance Tuning + +During distributed training, after forward propagation and gradient calculation are complete in a step training process, each machine starts to perform AllReduce gradient synchronization. The AllReduce synchronization time is mainly affected by the number of weights and machines. For a more complex network with a larger machine scale, the AllReduce gradient update time is longer. In this case, you can perform AllReduce segmentation to reduce the time consumption. + +In normal cases, AllReduce gradient synchronization waits until all backward operators are executed. That is, after the gradient of all gradients is calculated, the gradients of all machines are synchronized at a time. After AllReduce segmentation is used, the gradients of some weights can be calculated, gradient synchronization of this part of weights is immediately performed. In this way, gradient synchronization and gradient calculation of remaining operators can be performed concurrently, and this part of AllReduce gradient synchronization time is hidden. The shard strategy is usually manually tried to find an optimal solution (more than two shards are supported). +The [ResNet-50](https://gitee.com/mindspore/models/blob/master/official/cv/resnet/train.py) is used as an example. The network has 160 weights. [85, 160] indicates that gradient synchronization is performed immediately after the gradients of weights 0 to 85 are calculated, and gradient synchronization is performed after the gradients of weights 86 to 160 are calculated. The network is divided into two shards. Therefore, gradient synchronization needs to be performed twice. The sample code is as follows: + +```python +import os +import mindspore as ms +from mindspore.communication import init + +device_id = int(os.getenv('DEVICE_ID', '0')) +rank_size = int(os.getenv('RANK_SIZE', '1')) +rank_id = int(os.getenv('RANK_ID', '0')) + +# init context +ms.set_context(mode=ms.GRAPH_MODE, device_target='Ascend', device_id=device_id) +if rank_size > 1: + ms.set_auto_parallel_context(device_num=rank_size, parallel_mode=ms.ParallelMode.DATA_PARALLEL, + gradients_mean=True) + ms.set_auto_parallel_context(all_reduce_fusion_config=[85, 160]) + init() +``` + +For details, see [Cluster Performance Profiling](https://www.mindspore.cn/mindinsight/docs/en/master/performance_profiling_of_cluster.html). + +### Data Processing Performance Tuning + +The performance jitter of a single step and the empty data queue for a period of time are caused by the poor performance of the data preprocessing part. As a result, the data processing speed cannot keep up with the iteration speed of a single step. The two symptoms usually occur in pairs. + +When the data processing speed is slow, the empty queue is gradually consumed from the beginning when the queue is full. The training process starts to wait for the empty queue to fill in data. Once new data is filled in, the network continues single-step training. Because no queue is used as the buffer for data processing, the performance jitter of data processing is directly reflected by the performance of a single step. Therefore, the performance jitter of a single step is also caused. + +For details about data performance problems, see [Data Preparation Performance Analysis](https://www.mindspore.cn/mindinsight/docs/en/master/performance_profiling_ascend.html#data-preparation-performance-analysis) of MindInsight. This describes common data performance problems and solutions. + +For more performance debugging methods, see [Performance Tuning](https://www.mindspore.cn/tutorials/experts/en/master/debug/performance_optimization.html). diff --git a/docs/mindspore/source_en/migration_guide/model_development/images/evaluation_procession.png b/docs/mindspore/source_en/migration_guide/model_development/images/evaluation_procession.png new file mode 100644 index 0000000000000000000000000000000000000000..12392b142d1528f145d89f079b1860c2ca28b0ee GIT binary patch literal 23286 zcmeFZ2T)Vt`!yI*EFfY*P!Lc7>0qHKC8$W3-V9Zw*C+-MBm_}GMT%7EgceF55}Je( z6#)Th2?VK8kP<>C)DW^a{^tLmoo{x&o&9EZXLjbhGt8ajl3U*LyyrR3IrpuJkq#FJ zKL-c|;?mQ-X9fZt)B}MI_#8b9d~-f(Z2{OFcxt9|7gW)6fdYIu-jy`h)-oWD~7-?~cVYhsAMDn&5g9lX+|eCn|c%SN~)}ruHgI z^rozh>|_G5RjRAT{=&OrqE>YLC$la^kEd4Nr}z!5Z0}Ou5ydLhZ{8ihJ7q0}I^BW1 zEGBwt-iFK!gEz15&oCG+Zp*{;hV^lJ!`6BwDRdU+?$#UB>pr8tUQeS+08apHUqd{F zS$mVa@rPLZ6L(Itb~g|GJN)~k=mFqUxaR+}gC=WhUGm~XjtICvtzSp{h#<^l?uv1C zrrPdYKIOD`)pRM+m-cG2WW;wf#TMG6xR9}1Ge?>23SO$8{e}zJ1A&TPT|9Psv|+7N zI`Nc7*-+5x67objtTd#`n=VY#{QY``uB73E9&exp~-d{F5* zQ5w^Ddee|$L!+%vqaB)5(6;T|pm6=837~34cDXa+M)%v^pHKXmrK$}1{MgBVxoIoX zZ)a_~qd+h5TX)S2)%TvU3%ku=x=Mi8FJlns(-P1O9=4sNfboE#*C5?H{S(iZvwA4i zJ2M&LsSY8Y*WtS~m)3V@J37R3GJ8BT?SXqNX8`TFp%s59*Vq2>S9T;~)VJ<+xI}LW zDw`iWB`G~I9z@PEvY#rio8WMbNs=|rid5`=5d0C99U}F9)IU3=a!q^QLP^RY&Lvro z@R@>c`p`D^6qI6}vX2qv}F!tQdJ+rNpg@c3V}@ZTe+hw!RdeBpI7( ziAJrFF!~~~WT)rWY-S&xTG4QqOpK{dRx}G#=ZfQ3EPcGGHa@V7qgEh~Cz<{(+VB{6 zuu2n+AqUT9Ic0?0xH7`+Dl$-g`M}GR;itXoiO#XNipuO#NKVsOY(&dtFUay@9a(5* z9KmEb*^%|8I9@$UFACmlIvWq!?8tVDI|Qn(JOT31B%+L$O-?@>Fx4qE@61pQ@_P_A z_W21VDfVhb1x$0ol;bm@L{PBGt$RLwiBgrm)SaV&_a2_^N=2Ege)}Y=;og%8^V(EV zsPOyt>KG--h<}Ktmw2Vne2sF`1G3nF^7Rhcv&Y`gQc;vCYc6qX+6)fZWu6n{ltoeY zAN*oo1qZ4Wy*$jtk!9Gs%RB&zk5bkHjSxyL%kCoT#sfJSc`{v@%DyAje$f1+ay0CQ z&_t$1NXF_Yd0&YpkDJT$gX}Bpbwk5$9M@p(ts@F-!2&%e1QBdJkXT=_m-Tj!?F6`pisaVw_&1B%&ypap0 z_%O!ZlSS8+q^$EV<|>!uz4`j`NED+FTzug}@&@It*HA{#!d|7-L(z~kg#m5$#-sUc zplW5{S$sPt#w6&O*w>bi&MJL}-dY2D-?0=6Df6F0Mn#5u0KI6xc#*>O6BuWyay5Pw zRS8}5-+31e=~ul;=#@w+FszJHOC8Fo-Kv3*b{l8{tZ_}n_Y83Jr&u>1B2tihZ`#VQ zakUNRrR2J3c2(rag~zw&G8(Zeaq627=Ac#GO30k{(n|>7f_HDF1A|xDMK+rJpxl64 zZRPg1kd9!0#Tt!h90fNye>r)P^44Z3!}74ffdrB(&?qWXrI#_iHef787y^c+H0 zTdy7m-Bx79kMgQsuxip%Q<4&A#3PP)SRy9lNmKHVQaQWf4z=+<>f&3H;1SDfU1rKY zzdrRT+q82jkHyE9+hQ8m^yk{A%SHG728#olb|+SCML1oIZ%rrlS)VN75Q6PtNdX;V zsjy^6Hc+k&5PnBe*o2zCnqJNKzuw7T^ss}2S>}Jhb(SFT9vJ+ul`ma`TTMG!^hbd6 zd!?mwBBYwmcLEKQ5h5$i@#XL5saQ_m!=AVqZ$b5rc`GhOfELSp8F?SOam-?BdB0&|1#m<1xAT2cx%1Z)?i~wkf-fBT87Mq-yTAlnsRn z3Z%1peR4Usg2(06vfrYtC!$l5UDoN=hM^+0rk8rNwf)|diq$o^l7m$iX)$$Z(+9Y& z1;H4h&vycJ`|XILO;bp~xmv7jFm2}1m+%9~_c7Qg$WE$%&U#mgT}+dwSuMD%vK!$r zdNRX}&L{6oKP9a!AMFe6lC4|lwDF+Npj;*E>^^Lad=})RS0vX;?{Du+c`balekub3 zEk<8F##-+#bp9_bbiglv_kHv^*qwRn#;w)wXNcE;Zxp2c(ndL<39R#exuna++DHBO zWfV=U(hBEIgGR6!%Z~$(3^r|$ zY$*eqgg48+?Tx6B4Fmv?`*aWK$?7W4>IlPO1dJozB?rDqnU6evKH&na?@mB{@Uk?` zerxP$FATl)>l!9GBEKV1TwHnLnX?KDIyb$(SkgMzW}`g2MMfa5JAHfQ(vx|s0|-Hy zj#FFXL5?30)bhHb91q`AJAXZ_M}Hm+z5Hyol`pYNu0ci+m@K!Umvy}^vC!z}N}(HW z_gO&l?}#_X@WidZ@h$-PI{}aB&C$>}ui#HhQM2Om6R)9Gd{aAwijtRRPTJzbrJ0g$xx#_QKOK84mc&1OHBdtc`Gwp>+Sw<_SR zuei5{j{^y}p!#dpr(PzQS{zv}vivN#7CMiOO)Y9#`Nrw=tRq3Jp6=FOG8YIyZ4;9t z{4>DRpYOwe2}-w$rVvq4y~ZD}Udl><0tnTq{?^l(0&|sN+OwRt7y-VSU~`la*ZNFv z4*8nf&yO`JL$r_J8yB(v!DGlxfc&0sTR>92e zC*@Bq4I!QI%Q)TBNy!k{(N_+0|d%dix6EAhUSJG)(cETXUu$LKMu*N%z(1r zx|T6Zc6$&WU(?8bh1cW3!~Bbv!iY}m)y$$XEDVzA_+qaLJe#-osY<9Yn6NN(#Pwrz{36RkX|&O{L<&WwJy~P z+)X?{O22mhc45qyew2V2Fx{?kpg~4;mgm-bO)8{wqKRb znuv}0K_rHM~992hbaibHmZ7s9sxbZGI} z^X%fpiz7RaQ^RzozxD#S-vt=52H>5)i7+{v19!{>e))IFCYG3^0;!L9Z)S$=QjoHR zpA*rg)dB0m0^rbRDg%_SM}&_va`tJcG-}v<3F_Nkp+Rb7bB9eGTYLtz`Id!K*mSaW zI&^0ZhF)*Zor53UJseTM>r^ug zDfh^p$$Gw2KaTUF6ye8g?bzOr-LT_Z$WaZ zJRNH2iDG4$Delwx?|sc(OVOtfJGXaX4CuVZRcmG{ho5x6$_t(cZX+16OUzNxe_=RO zL&O~sdUo+=uXG}9wOu%9p_*vq-K`4rYfEVLi3!n~qsjgKquj1<&8^U0g3o@xKniP| zKULF`zh=-$vgDj5CfoYei_z4Zoccpu`N{^2K5S~b`q9*r7vYMJErcDlb z7jBmFbX9t5)UQF7HkbV!&rF6d<}bRq=9u%|s;4hCXp%kXDI0Rpn4e!eH6*XDlXcqj(q*25Ko|AonztuU z{(i>ED|0ki#RXFD?`qw`m`IW|mk=0xM^M)H2oG_qyU}^Y4bm_mNzcM<YbNu~;}n zad1A#{wG4a>-m!u6?yM$>^sSfk42;4;m@^}3JqDCn(G5Q%Y^|WTbq!6pWxA=5;w$} z$N4U+*&(FSRJtEFwp`YmUR?SmW?#9sF=cR;u}QmSSocK3&V_scbX%ugK5R9*f?6I> z_@%^oZ+q3-P-C&0NJ>`IVoav~*i&5U;$$g^8QVY>}TDv8|rNz}&d{UO05+tJ63umdifv zlthU-?oIMFkX9F=2vv)`m3|%J*E{XPh|rG}&JE0cy10PfXQ!d&xkY;*rgfh`Y8tORrk!UqX=UD~ZyvI=DG4;r_$Y z+CDiJc^1e(fik!Vdd{27PqA4o*nF_VwKGwWBGE)!}lMF=J7mH z+}NS)Td_>3jDE5mvgSW$Sd?5T8IVBpFW04e_mBL;@8PEhucBu(vBi3)lI`z4zxXe8 zFbjSW&nDp=b*en>Uk;&5F*M68G>RFr?tB+T`VVV&)9YY|b@&o0|yfT@=ol26=U2fN-&{Y znXGodEQMrCb*H|7aW0mg52H8gvCS`#Y@vp+;N3ZwmET|gh*~QLF8uo2DUCVKHzreS zXWFa2Wp_Pz?Jo?iG%|7Lh+tCef!o1BV^4IjyVkPJ9@hzL? zYY$Eze#DBz*}*euK%N-V`}xTfj-W&q3>Yx&06<*=u;Nu;dST|1;ahu*URq}`X!bB* z*fq7#88mY0&S{e)ycYo$65jFZ*qH=id6ujPc%MWTSpqZGV8E^eyxS+AsSNy?$>{39k%=z`1Zq5S<5&3Af zg{TYux@&YY6MS44vA0?eSq9}|R9G0v5t{TKdqe}^gqI{glIBR7;L#rKffw?aYXG?QZP?m>KYhy(cjU1~$Q zX3p%D1Gi<91|PV-q!sB+rzofav#3HrkCIKeoPjc;7+KXD7);TEZLK_$*%cie(nJgM zrEYlT3~U??*X{Szh4e=AE)+h5oT+8ZuYT(j9!_pUy@$6AaWtO>w9wus%k? z7?Nn&tml~~)b~ukUBQ06CR3%*kaxf$4`gsBeiKSUL!6$ERMbMbW2&Jkz9y7UDYquy zhJSY+NwgqnAyM#RB7 z)8Uf#+eFDMAPo^N;^tS7$RbGYHzvu^oI)s**+!6jnC8hD%7tsIDn9sXS>GXIn5P4! z=yDce%Y!*}qE$gk-PcNGwtRioqN;QVN=5|)W^H5ie~ zcH@d?;obwZ3eW}N2@w5dxDYSLZe4;%^2H-Xl0R)6sVOE7FYkh8|%w#|9x#iuoF|=v$!s-jGLHE!G+sljgw%Lhs#yF4 zra1v=gfso&_2vl~zQx6u%|U=^>&4&R#Ky*+OtPuJ>?&*eDCcbabO*;~;MkLP*wMwi z_)D(j0j_JmB^S*u@&tA6)&fY|glL~)s64J9IA1{i*vQx%y_EG%)s=Bpn%lEm$fst| z3b|a;=q?6CEuT5}LidoX1?UJZ(o0eywJ7~p@l5(#l2*LMkxouu#Vy=F?I)ECQ_?jQWq zv^?!Oegp!~0@Fv?kT=nG_16X2u-)0}s)B4l3=|?Jw%>Vq__et4=J%=#WuN|hYB|9P zuX_CF8?0EBvsB77yZ~;Ja!S2FO9K)1tg0aJ?i$-Cw=JML6p6kTB^-7W!V}Kx9MiZ2 z@b$nWHn7r|MkJ0#Flh1Ld?;B_ZuHJV%K0nl9~FM>dDWnKSBk7bGn26_?zq!weK)0? z8@zon>tvRp}3XG-55D=^<&Zd|`h_Dbx^kN$)ek}6jD9e$8dk~L7FOM1QkK}YBq zBP(!3l2#?bUE|eiPSSQg-+#v5v7#75(%%|i4pXk*oiEdT7(Dq_{0egy!%SxZA&MOc zo<=D4=l*CBYYE9gN2*E-l+UEKGQQ(y zN*-q^inFfsl|(}(TaG>!t3#PYgG%M!LEU&RN}8Ch6rgHAyGqZ$6e#}&o0yHbFZO1^ zH;0iJug_;KN>&;W)O^>e(r;CBUy}%Cg(YAQ(-8Pthj`SVWgz!OLQ7@6r2u&NSY<$u ziy-yb>}1iGW4Fn?!d904|1rFL_xG*a$*b8VqMIsYia=N%f8-R@>@=KQ)c zdv9~2X)rP>``!HA3Y3=Bv2Uyz728=u>u{bUY9oC1jh5jMh;h=aBcGN1&yBPwp^)Oy zRWr>u9B*hd`o=mn<(1Cuk-i@4s7!zT2R*fAAR`74TLaJDCn+V99fxi1G2I#XB*ELp z4UPWmBr#X(j)sysG9Gc09B{DaPozMR`T8K8X)i5k@ceXz%(>>=&jdF1h(PC}x+oxn zbvK;-M2KML3h)g)}j;odOwj2=-{bc*cm9ZS-XO$B&Us5r0q9$iL;@M&}^nDd! zEsr)fcj?~5wZ32$Ef7f0wqefo=W`b32Q#_V2R;R~wRba9_HHF>v43U6)b`X*7}4i9 zM(ezNYAE5Z>^-Vh9FNfvCk2HW#*1HLN3O3KPXxry#Uv0k3cYvjJ;qbob`-idhoH*T z^<7zSyCDh7aFN|^8;DW4Ii%oS^MStIsoNbsK<4XYpXwME>~XR#!A-9Yn`F?nrzV-Y z`F(c9EyFiu0m$|hv&2U#_<3ue`mylF(15<@i$Z1PvB#P}1~2W~NbM^x6U8$s$nPgS zyhc|88LNGbdHBNZ^xWQgFgo2ipRq=%u}IWb(y!XvtZDVLzhTz2Rw%q+LofBTnhllw zv!sN%P~(eK62y``vt~Tg(3^0JSvbi()LQ=mv9N)-J8mOI0LKfnC|*&znsb7!HitMdgfk)TLl* z1xvpGdtd>=6v+Og~CkuG>cqiJk6Ad`XRhaSRnlLqfHbp*{f%8=xDVC$ujqN(Dk z{OJM8jbLESW^1#?Qjs2Spi(24rqtVqr9Wy44hynLw_PKX6@m9M=?dr?Qzb$RzR0?l}jaH(2 zzi06oZ&8rT3qezH$<5YcHsIx(rO|P&yxkZWW1!hWUqnF68}Z!MGb4ul)*lvOu`6Ci z)KJ;~Fw;T5HXER_I(g4ZqZI85PYt*%aX>QYvU|X^obDi6p8EW|0{+LU6)I7|8(t%n zF_BW^=mEyP-JX_53||BA*H30z0DT#cXwPBp&C<-hyLRu3Y)2PHP6<#Njb@>R0+C*X z+D6*#SvS&JGx1gh=cgPiKQgvC-_K`ldl~uXL5=6<*r!5FW?#rU!F*gsKtp}`u*iN7 zKa>YAOmZ5h1G(b(H6RcEIRzaDrjsflJ;@5YxO{{UZ)dGkv!&kCqO~8J2+1he!2CefEt0_P;&l z_z8A>PRB{yWrE$%K4p8dw3j-G+JNxk**{J1y)6}Bvz?Gf3F8gp5U^E4t;R+bI0}6R zOudUv?2tc$;DZlsyAZDjE8nnj+-qX2TBX*24G!IxZ7({t*9?i8sd?1;E*4NcR5`!o zQu?t+E+IDF=(#B(BhY2jd}=TFPj3JhwIshMAb>xX7~#)1w4GP{)O@z0+?Zlay1u$o z_C!`#-kWOb?uj&(@5~}XJkvMD;~c>gc(j5Yy>w$5ZXB>{mvS@93>7fKyh}D{w{K~a zr=l{R(fOgGczF^s2#{9brW)6KsJ|Y!aNS^XlVXH5nx_g=9rQJ7v`N_R%kr|soxJrc z%b^2Uo3=K_PFZg&<#lbM7&W^UYHM$$k{$paS=$%VX;HGznjLb_TEZ_pmuOn$w%V)g zG-yw*Xrq^|ZsSfO+zr>Oxx<=k0d{3)1piDueds>W1VjJT(%e6nttZ}|&Iu>A!q%5& zsZohrukI43HfF{TZM&{*n(oCpwyd_2iQFR?Wop4@yN>wNiJ<;4dVuS=j7wJL>RWf% z<())>oUYMYN5^_+;?XcNT63K^dbvrlbC(H0p=E$1%f1dR1L;zXz(24_4f307d?$S0 zMg8v^`FGQVm)mW3yWHMx-H0}RxySW_57j`LW|tk&u+^qhBo|kyea*A#w0_$5FW2!y z-^u2qGO0?@j$jA((D5Jo4bL;k!qmP79*|(*FQ+snCzTejb08VW6;U+L8b^ zrCW7q5@x)04l=o}x-4Wkxo+m^#w*-7&3OX$^L?W`y)Jf5j;i<2Yf@Rl&e>FaXhy%(kL5dZST_@4P zdu5Mac+|+7<9PbE90!C$=8%*J)8CpW@#c~r9+U|R;>QWc9BVJa zfC9(?Yjb^{PF@+>Ye%u==X|%6ViGJR9(N{5Qck7=KxeDL@AXDi@#A?G>-ATFysb^- zVTpI%OW)}{3Om1;arVd1 z3ei|YFjCzTmKzx_X7se&|FmR6WZCWQV$*yap@h(_XN?RygXq0XNn*Pd#hePfx8&ghgAp!1HKdz=<2xm1esl`az8A`p(Y!#y(+<7zhQ2;F!lp zBU%dNqy@Y3ruSwi#fMh6euiKL*Ski8Hp%Ap6B$_T6}r|?kP1R>y8>d7k+QR#7}Wa^ z=CBjc?iuZv8X5hD7>|D1^g}&od&8wL=NbZyB?-(5mBMiHJ(O)P@?Du%Ya1p3j`M=#eCL~f(7&;p@eciT3}ud6p)a3CDdKcT z)e1cwWR*{yKH|S>s43gtE02L)ZQkVJ`W+HYR>n1x6{Ed+1#_${IAkW*m74LZ>sUgT z+Ww2jq}e_2%=r3S*~Gf+j~Z4Gsw~dlxc#1|VW>|if0@y!UoEKkC{qwk^)(#*h;}WB z&)fLA3~0f<{*UcLWXyu7SG)GG zU(&QDCT~Nf_Gpdd?-Kiz%ahihNlK@~AL;Wgz=|9Fhi?vKe~`4lps8Y35G~17+WIF- zIQv9EZP&CUmy~^G%co64@)Rr8pIQUz9EF-$11V8&t9rH>RRL#_)76cVTk7!5ZX(ltRT=&w&s6cbk!h=q1X#H9d^ z>lS{x8WQ9;kD4fLU)ip)YmU@*9`T@$)UN%s5!&@1_>za;j`q`qeYJA1&*JH9!7M|T z+AcQdN|QecChBL+3P>GHe=T<$sHhpWr$_S(pvBb(Oo8XG^4Dln_OOrnN50OFHRHE; zbK4_C6vKS}ky^#Xte}as6Jj0Pt5Zaz(LU0+AJnlu-KDNZfEL<7xCty;{*E!- zkgoiTZ4#9d)~Zo&HTV$R?7F*OzrT2kZJhho1k_8ShH4!d^VW&Lf^+H6B`q(NRj36F z`LpbN$I#Eeo=v#^b3hqT_qI-)kYF3UT*PdNnI~N5XG9a-+@~+g)34|op9?LQe6o0@ z20U(MsP=?6Ef2sXlgdtnIWoAD{`)0&f`NRK0xP?1d7M2JrvV-~F`cub*u@;pfd+f( z|3qo72J@gywW_?n$KITL6TkG)iy-XywAk2@_OLBGUM{G{e20?KOy|EH*I6u9rfZXl z8guvFL43y-LOi=|RMcaviC=c&prq-NN8buMP7CIr;R?*t(SWj`qr|Z9n<7M%>i;k6voWvdqkux&1Bh zqTmOTO<}cSwp*yykxlqsHToU*UMM4{47oV!K4mlOwS)MzHb439;$Cf1wiT|8b{LN* z)#NqOteE-Q1;}MQGpwDR&#uq3&wK#x46)zt4XnVZ}G3v$8Mq1gl!zvkDd|w>7?8EvWugHtQrM(+Iyi zh#?@CUZ2=FT$;XD5|Z~afC^~Wr0F!&ARq(PP*b!3&o@4`+bKirbU8+eF8gq5Y6h$ zrkaEh9_;4i^{N~0J@=z_wQCy!b7izPq!4CFVZC1Y)C2>$4mh>kP9)?T=6YgHQk1uK zCuOQN**H00!5CVeymZ!3GvsCO*D2{KNgaxh6@LXKJ;l25+YGJUD%$!#fHk7 zDtX69MKL47DC=ZMvezEFNRPv87f(|DD{W=W$PoZxnSXFT_}4U0%+9OTBu;&v>Hln(Ah(jD z^$ZZ+rVZx-JU<3jY~Bx(>Y3!hpeA4kC%fLx(_px-sL5Mx9Mr;Z@3j`PD?&BNk{oK>2HN?R^=2vCC!x|nZ1@{Xyd8<0c6CT7s@Z! zNkSc2O}F}cAci1aSpS1meJ4TQqZ}2PEVsA2+CKPcbAVOhIFW4aeLmYCX8zGKtiuP; zIM}~P-3?a$;NTGEBgO&#T0dV}#*vlFilIbg&qIkYoCtODS1!Cb<&!aZM^un%q|Arw z=Z?n-6jIrKwAN|dQjc1?U^48bSOCW4Y<6rQN$D(LX}=Yxi} zrCQaS^wuaAA0pb)G35AurD!F|{xcf8{c$)r-l}BL2rtk;`bC^aGhcK|``cp!W@NqB zt7^^tvVVoWN$LLtuBu?rP#T6&IHKkS25G}YhdXFw6=~G8t)RK39}G2Y{G1>?c=Cez z@QSucnu)x0c=S#kNvd5E{JZ}p>T-K)j>clBY&UU54G|m~)OU~4;Q79lLcl}FkPHNG2sVto*A(l&fgHRD8M_$*Y zjJF8HG~r->sI8LGGWNUYn*a#?(IlL(_0!gY$f!rkm4$lCGP&_ai{>de4P|jl0biG+ z0%|^yk)7{%0*0ZQcv+@xjhx((D=J_x!yE;ua?s`S)SK<=3tLX@BTQ&{RWOwhe@T-x zvIG=(##aHIF3Nz152IsWRlGS?$xOweVKQ!ArO{00Zg(p_2aSDy#R#g9-HH$|6b|0Q ztr0+&AC>R6t~DF4Pr5R|+f#W@OUD?uL*-IcBb&OHQ~wK}W^cDSFnt((3wgzf_n3X# z_bTOh<@w0>y_#%tG(Nynw^}y4yoGB{Fdi~+Q^M6oV8mBaQ2(>W%;iU`F&~<9Y8RpW z3TWpUsEvXTpQ8LkAZfQcZgo{MNYm=?FNJr^6Qn_XzCgG``~4PP(D0Kjyr7^oz<=fL zYt%M6lJ|5sF3)%`IGTfD42Q#Od-A1ZC|Imx60G-$ll;1CKXbycNO*LruF*|CP z5q#6z>pz%>8@GY3xQ&<2s#2LIt8?Jaj4DfqTu)c1Bv(K{2hxX$}#PPCRTWvM7srWtD_QL``Cu_?(-NG1D zN7U0&RKWLtUGx^Ak^BogkKJePI=0Q$*CNSrDb^^bhDaOKY9lqDX*6FAHjLgHl25FG zR$gw`r@PFCJJu4m-V&8$XJ<#R2Ue62KQR%-(h2*USMX5MMlC@}0DorM#a?$Ex7~iW zYxhqQm3GkV+ak*Kt+5mpw#JWewksg&;Ig-|aL0Z2waPR!BY+Br-xA?mbcMXSZAMK6@22X&vW(kT6B4s;X1EgTEF`STxKho<%&7cN!vaMuW*me_ zssTi0Ra1bv2eX5W?Qd@X@A2+GTTtmb=*nP5*N3F={O#WZXg$*<0ZIbnYBwzFu1 zpVhY)n_W6eljSgWkL=|1u;3gmUsq4&hI*&ucxDt;M}71{NhASOGvo?eV*Wm&q1(P& zu{za`()2DqY{by0A}AW5{OYGu`mCS7%2OaZnr~$E1t|M96v>;#WR!~DTNHL0?`K4H zKBI&cjKQO4C=PmfjQ^I*F4z3Nyg@yG5>?AH(krVB=IFjvLb_sqf7axKf*G}PYI51J zPfu1+QiWQ?qgJTdt@T=N86Xh_e?G@*GpkHN45yVT#$s-)*UVOnt?PIk)@mb*9Qu-Ng|8B6F2ZpzL_< zxVxg;N3GXT**Qz+q19!X=rBh>IWD-$$1o@2;XrT9sC zmy|eR)*`=L%I6*C#WZ2)S@xn%=EOYRz3?miPE~;|1c0QahV7;5z4zz^-lbVkjvtnW zhz|-xb6lQxiE+*z*t!8)Q*h%nLKtq9>|*(JIVl;S9J41#GI+RerR|aGmw?>EfRBvL z8B~*Xrs}kZp^`V^PX%E0UV~?3PxMK$R?!Z>_;ejR?a&w^;7=Aj& zJ4Eu=v97-X`c>}5rE*wNUmW48(*%E88c8m;v^Vii1_UBUvzb1F4Qd-${;%*<^9%% za3|Fe@k??c&JTUZ<__P(EW)kWa>|c@NQ#9{o!3!O8l59&%rjm}*?Pp-&foda<_mGf znA;RD{GB3P=jqgznZ{m|wflTkkS%;yhKb9IOiI`f|vf~SF(jvfI%ZdK; zZLxv^*pD6#KW)B{1Vp4|yoSxH8-0z}H(*K?*Ny{EW{E?>^teNk4pV$T5EKjb^aYZ& zM{F+@T5=Rwh-4G)%X&jotI?QTU2bGjk0+!s&Bl)<%|RxROkZv4&F?lm_N>k^$AT%2xLw0QjqZkUE@2kY~mXB zo;HnW5>mm?(ELG~VY%ZVxJ*e6uQBg!N2ioXJ%?*kJ42$mK@bakw%7YPZT2rOtcHGC zmgVjL|4eoDKiESV`t~}r7sF#u7)4nPVsa8xL3eG2l;iylc+&!Ltr`Cl$4X|HVA2HO zn*`xj&$`PvG~PVVQY1$Pe)*sM=>OK?@kQkZVt?-+BLEm~<^*;Fk_uWtpn*X^=zM{# zkk2vn8cVpUau$^C2zcHINKr`RvnSU>w^v}t7sCO?TX?`EK+zd30V*h$VXSKh;0MTI z`Sx~LQa;jKK+hpAsGk66^~H_VR`01C2Hk!NOf1kqR_CSe3}tyiDvVX*&k0@AV;x%u zW;JLanGK0v2ms=%E=$n_sJFsrfSCd+E(205(SK@aTmzs_cshFk)F;D|2L`MIw=2qL z4>MJ%RqAH$Z{c%dfn%9A6&x$m(B|_v8=tDh+K#pipM+RNjFx;8&#s7e3P5U80BgY^+&sEgVq{C$NiteWo&YnzjYKasBdisetvf|HNk`76DWV( zO)Ce8nGxWftO4XmfMM%C*&EYDkkn(jQcThSt4VjkbVNaK*wRyq$rCGyvSGOa+@EJ_ ztL>U8IDf77>anRfa5~zq@^Tg%sL6eQZ>Iwqww?jV-DMLd=C=UKFpHq)i57ZjA2c3d z&yA%W)*q&UKpt0Q`9z0b4|QHS+rLUdA(@^d(8IjjGPXE{_w;WsA=P_l;?z@EK4;cx zTT*350EtKn|tY>g-zb8D6~y%|I7P7$T~ZW>_QAMoxF+*;EV> zInT&H7QxA`@>Dpac;>qH?w+v0i-cX!r-~B)!<_G$S7V@}Q|)owEp66mbY*J+6n3gR zT~YeHdI$`=HSrv?aSH_Ef2*t~mZP9INbZ!{nrq<}2Ru0rDuqgAfLJ*gT`j~M;02M` z2OlU0EsgcvTmX!$#FExxwj(vk1-(Rv)8X2m0V7S~OK$K@$ft*QR0=m_%tJhQq&nL6AI2ed+8X2#9J&AB;}l3e4?@O{A*J)C`Y#^^y=Cluu2 z21pkT`~{Lv!~I3_etD_gO)_ICC=t-#++eYqiGbEm8Zdtl{O=@S#w#4Zb_$?H z3GG}Q5(8Gmu_MCw!P_e>Fnzd0*F+*>&zYE1!`2rT$`!N`a`X^rBUkxXb5S>Qua|ke zC3!4_T0Y)sx^|^evBo8^p|?O|yLX01C>G+3WqN#Jw|ns(`wO15q}kN8(CP?}n>|8<}!eidCe4hD7R z-R_h^nzAcc+Xq;l)24cMT=e{%dk4uJj<}N$!Bf`wr^z8Cqu@)Sa@DscoQvMt>}<9H z%hH&OP4dEhDRx>A&wh3pU}=&Si;pLmK8H3RHm*(yE6kv!bVb$76{g<3oh#0UVA9FN zl4-6{ud=^S1^hlPR1P_Gq_jj9nhy!OR&}Y;3>=@!2G_Smp2b(Uoao@qVTJ+$kUc)w z_ao?blVI@Q4;?_s;#4Hy+FJVuRa!uO_gQ#C%0C{kfrzrCtabWd{;AflLqdwu)g7hA zq4U0{ZOjdt_2xAm-k#&@_7fK_f-G5d*+GAW{)l(^!E5Uhs9xzR`K!wY6>vxw6q1Pf z{93`VuV`?e~IPT7Oz44FO&kqrkfMq>|+jxK*zG7_+ky}%?LD=!m! zDLO35_<-oC$*}0r@hlIF@oS&T=hnCnuBdD(FeKVAK)Dc~hQ(Tn3Bgi?2c#g9d?Ow3 z%c!Ina=DlNJZ3&=;`872@ z@dfE)F8MJ^9E{5ofRecnk^ivf5B08bgsAsDK2Z8=%($N`w^ZSQ@Ckpwfi1>Tf2fc= zX>_t=!-D4ihy6^6YKIY zAU^GxTLbcTcq^;q?AKlNeaZtaDUTltz-Uc-(853CL_$gWM0yHxMti+bRk!zTowjZfyR*8mCe72%>}rAF3_Bxj^- zJ;mAlJ-DEwlH`1FV7t1u6hL!fTw zVuOrgwhOl%^F|hg08#YV$Wq^kjf(CFnl@#%80ckT^}_6wm7M1=6%!Q(HC1W=*7vkn zOLqX#cDRGrSz7P(DC6he-A2bjF?q^*9>%hQq8+@H(Skh4(tvgXcAWBPKQJa)VqkKk z(7@X-CXBOFg!Lki0OH!6gk%FLo3o$2K3284kQZ7@zc43WDqC*~9>`5&83X#PtHa`x zW}=v5vtT~l6fC@QRM60 z#yHQffv+ssw^4;(rWt0R1_WFa2cG$s8$YNU_iOmqs<+jvL*qq9i;S-i{wkGvQd}@a zG)^4cRPyZ4>vK%q#~gv8Ylj0z|LcFX;tS?0kI{l zvN8V`Ai8x?Gt?oBBX+TNH1fAsT+?Rha}(?eU{=XyBAD`mWj$|9^AoayhQ(NRRgj6i z@A?(JT7#-F$+$gMcVD%Ooz!8sz&`vM(TCw{(lU#;;B3<3-zby^g#5rZUs4`Xz z{smg3$I46w%Dl^bKiKlfzEKHtUK&$i{@mz5LUM2^=?7vb)uaYka&L-!wz_iPc zW0_X7(wC=G9hYnzau(lufuW;|6eZhahVi2d7$wexzg?<}Ji83#;R2IkQAHX~s+_VU zFW-2w?YaI}({ktso(qy?fGg~F{yz@|>)k9k0KwEu8g7{C2oBaXf^9i{o!9<$bo_HQ zlq>+Hxw(EN+bo80BNc-O?|kJBi;u0r)N&>$3EaFotWti~FS=Wq8=5S|LXJmc2R<(t zFzI;8z(~`|-n!xu+fFxY(Xxxc6`}uh|HvAOZ%lO$!DcpAMm2@dREGVSt)39~OOn7yDZtL6JgPMAxOrb0_Nz zJet~PBrfX2-e;|4{NCT+ocfP~Ka7_%*JpSF#=^b;c7b9!lkJiEzUM`KtNXP(n03?& zz{rpUULWp_{NvAajxi<7JlcW?*`=hH9^9MYBiTrZCawo6*zOL{0`+2{U z+{|8$Y^vFgs`7fB5VZBJuaLwbYW5!}$BVbk(4GudtjvW!VL^C{Ac++IXzG$Y;itwN z(C8c*fB6O2V-EI~zesh@>%;;4`#VaY(p-IV?rXYf&%`WXUxwk|e`6OiqpVCx?I7=h zkFRK?Z78#&8wv33xD6D0?4K91g&dFO2tBkdu4jva&#CoWemk4E?Itl=i?zc_p6af4 z>3onNq`Njga3+8HZeoCE8#Dk4DH4|Jb`n!6tDv>)z;mBRIsu77WpYfB3a7>pI?&KB zLjz{|K#!lB#+ToYkS?1)@B6$BT-F1o5!Y`Ynz|v3WzRhLHpq(N*#ND{QvU1Ajs3mj zL45$Z`3rpST#bS9?n_=d(99pw4GI051uQAbf4Dv?#dx>!AFIb1$v0|$f4Qecw;kL{ z59cQ`M#*OI*>~A>qxf6ji`r;w!`3%rU7R-y2j&GFTNP0BAH$(WxNYx9CKA2Rls{#4 zeZkJqrPfp1gHF(&e;y7Uf6M;UpVeEwA?IS#U6|=uG2i#ZUH0Cziy&nj0NEwQ&ToC* z&uAQj!nIiq5fiq=QkaRzIk{VwO~+eB#>19cpIG12Q40b3lh?m8TDvW>3!zxYAwhsW zD2b7+Uok+lQgXID|1>TK;&Rtt>^|0-dW280Vnu(s{t?M8`o3;vCKg_T^~hZfkyaTw zR*Ic}?>+_1*r>An4=xUP{$QY0r4yI?wN<1fnPL%${4pL|zss5}!YO*lY*MlVS5|08 zY6)j4I=tC)(B=!FR8k?oSYkoOE{}dbI&F8co>=hd9OGR_uqVJ3{SCrzaj|U!ex*^a zgM~&8%Xj$=01%;!Vv`>G^otPOvkPDS_d5d;PU{2pwi^kn0JEf@1Nek$b=+5b$5;Of zZVS)d4&zX>qn$)a7u3U67wH2Pq??<^`BghSXBmx{kqZ+Ve70rnG{Lm1tS3Q;ldQhD zLkC`2fsAtR_VyZmDwXn4-lC_+tW+t4y4R#4V##as+jULQuAr|dY3C03t^GCvEmJ0R zAaTwPBY*p%t4g9aM&$_vNaIffsL(^}>C(7czCc8nB1>MiPD8iwipkTYo2*D1QBxFG z>7cVEN=+VJQ*z!EHSHkmoUnQw*K`pNX>r1A;6)9VMte+b9v_n@?zM+o3_aO{Tp)M{ zhiK{kx`^WO*ps`7P+4Ba{RT#|H)X=Uq^dd|Y}Mo`>;k8)xsw#i2aj8f&l>7R9Dq3l z;%3U8twug2B3ORHgt;jT)W?-sc|`d*Cr* zg*I{86h?X_D|<eiVInlhYna@tA+>03JXFt1X|HK9W2X=DvYpU2o=D2;|U( z^O-MI-99>{G;){y>CKaf_8$WFv3u!8DDBVQ;%$uZNK2d@XDV+W{ZM#@HipU&2zRvA zu9f)wdv>rUOmJ_bG>u%iKHjgrB*g>8x!qitnV}7bI(1Cme2_+N<1GaExY0Dy#hw9f zM?7*i+s&X|w1R^WUU^%O<-}1Hi3>d4vuSMfJaL1lQF~@z`-1W@s!Gxt{8S=Kq@}&xc?(BhFfJ`+9>Wnpf`Y5(4V# z!t76L!o(dOq?1mNtw#kNZZI`lVtvjB92%a}i0b?Sj)L;<21OPOtpnjn+(wpeL!^s}$)MRz^92uu*ugV`)`Tx@V3xj9Af@{;)heY3li$KvRO0 zoVhhx2Wss!2OOca#mgM_XtE!=j9tFhzUlD3M8VZ!`~>NGQmewj+?eyoE_kR*F>6Jr zGlQW!(bmDah&0sC>JgV0T49eF-eU^#WfvZ^9N!AKA=2p46r2ygUP%6;zl_c2I;geo z#~QM!2?`S(^4GAfvujc4xiIi-Q(ru8x7^nJW`N7MMI>ffm%i9#^!26yY%kPU{u7|AaU~@#2HAPjR*oqnga4p_H;hf3fLlVQ4N;^ zcQ!#c^^7F8-tv}{_QF_!aVoT>l}7d0mTH*V!m;muk`-HwZqw~T-jj3=Zvay1Xda%o zs2D0ydA3C16bV~y@7QNI6&9vdo|8Adz1rXq@*Vlnp2b(MoS)m7r?ow2tj7=MFlm7& zi5~X0o0W?S6M7`$aRj9;#|c4Mb?9>Y^g8x<%CJDxD0%75CQ0ceUT+0@I5n^jkOuMW zmhzNF%7%LmK3}c+p)_aHODELK=jQWWF!(i4*b+3;cgyPhw;;!7I{%akb+eR;Fu(xi zueDb|LbxO|CQfyrb3}ztWIjz=nn)=0(nLSb$Od3q!-o36YPar;M_x%eL+hgM-M9Z% zW3W!xqv*%0V>zmK48|^_OCSEio4&v&F~hkWZLgz!b-tu&#$|LIr-65OvkJlJ1#=#{ z1n377;>Y{K98-#&yJG&Bd?z{iwsLt_>dt|1ub=KE|eja6=-x{Q=yjIWP4`MNG_8c5ogCOWkXUfKmC zMJGtY2!a+^W7}I}S|Szj$+CMGwK^qNPn9>Gu@rB&S&;WT{Hvp)z4Ld z_HoHf5xd)|2-M7T|Ni-j5-wHe$7bOt&Y2Ufoq6!mj^Y#1qI9y+%qNm4_3 zICSNAK;%rp^G7~YH?`)ARjd6p6{d?6g2M6Vp8?vO#myQxORPDzA1m3bcC)CRM6hDlb z+sL>l_gW#0{u9L>=RD9 zrFly3gWF!(zGr*Rmjzk&SaK&Rpisk98zU2^0eCKg{$>|itrP@oMEFpz(oX=HZc((@ z93kUHc(AC^!SniQIwLdjGEqTpf}S+cETgaWfI{OgYT==Pq$uwi?i&tD99H3l!%?HO zD(_y%31z}otmRh_{?gHO=qVNjpY_r)+HH)ft#cl%>lEK7C?oZMOWPAO&BnjowYL?! zt5{cDJHvd43sBX8XXSJ zp(xJ@B*}!!GbuyovDQr6P@*@Z?Tp?{TwHzCkYOv_YV`^qdcsco@Wef;m5S%N_9J|j z3QQt*Zhib5omUmh11=o(74UE>E_%$ct07x9XT{y&y5Kckus-h16D=0nN7Wsh8%wSS z{^SG&^B$FWwT%Lj=1w4MJ%Zj$eL-z4@b$_(nOE%_VOy~wL(F-sIyqrB*gWiU%i%-b z{;1BOjVgbCpX^T8y7!vjw24W?oL?yjxg^yLy7(g8mx-AX@A*){0gBbD2D z%|VK2fUt+V#o`H12B%PVA0|ZO07|K*E~@K&Ks%166Zt23HSE3CXBM1pRVsNH)~Elq zq<`a*s+>Xopxewq7kgNf{KM~S8@#ALv2?uIg+=?5{AQU+3OMB5jXqW8FM4KEFO;^y__9F7aE$DaOiuw?OcwC?H%%y%!EPp(2yJs$cx<5!x?bW1UX(tnvk$ zrvrP8N%Zr?s-CMn-U&je=T}0Cz&cSuqZIwLEwKMC^aP1j5)1VC(EQEHpffKN1^HPu z_A8oUWy=d*Hzu73TedDweQhs!>ls^i#ibMPsEO| z4x1E>5(6RoB36;asva1!D_cip7PtzBO61F)`M_zEVRI+@U1ZX8GeS}4>FtOEOXs^@F0P6+cHjf@W~ z`LHIG9u2kO<(7+G?(~XH8iUmdBixN+2S(!WgUS;Io1_M0|Nf^;6l$siHM47j*9F_Q zg!3lg%@uX9A%eeu{`1)fz5f68%~vu{ZS3q4|1c}wvXk%;TEo-`akbjOIp%)=ZGH)| literal 0 HcmV?d00001 diff --git a/docs/mindspore/source_en/migration_guide/model_development/images/parameter_freeze.png b/docs/mindspore/source_en/migration_guide/model_development/images/parameter_freeze.png new file mode 100644 index 0000000000000000000000000000000000000000..598d2a7f9a02cc1ebf80b1155485da3b1b66506e GIT binary patch literal 34703 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 literal 0 HcmV?d00001 diff --git a/docs/mindspore/source_en/migration_guide/model_development/images/train_procession.png b/docs/mindspore/source_en/migration_guide/model_development/images/train_procession.png new file mode 100644 index 0000000000000000000000000000000000000000..8a0a80236342455ab1aa9f8114b91a2dff2bad37 GIT binary patch literal 39117 zcmeGDXHe5y*ggvDRzy@lLjdrx_r*TWJ=qHvQY_)}aymX{TQii?95%k&=i)8k=X7A|gZFi?%jPR@b%;D~>@OksXIFPGgig6X!EMs5-0Ew_lgP9FEoxEbkllBivU9U(JQ ze7P7ejz2sNsK|6*QI+(t#hPGEufY+J|0llG>Ld$%j3O7cGt4O#`&h zd}0(Cl#dN{!#avF*yZ_HN2>He$;TbedF;mNFgY!XhC(Y;s?{Ua3yMfDubExa74^kx zHM65Bph5aC{=FFbj}e0KkrhJ8e3W)TrRZe%*euGj1&p;(yri5P?~|K$-{lRC&bdEW z&b7`*{aV4ESD(SzM`#M0Tz&tsU)rRYmH*HqgZ7vS$Z=7FgtDY-C!=TErf5%BO~#l- zGRGaZo|3ZvWPnkuvrz205E55pX*el)9paF3SnhmhQ9f(a!f=)e^Ms|hvNgKihtJi6 zPd!Mu-mQC=*kG0B_I{r>4$9R$@ES;Y;YM59p(G?1M+0j z5K5w(5`)cssnBILET-FZh*uC?%y zvzQuS>k(DQyQ+eytMRH%CEak#n6X{2jg)`&!@;`2jT7G4RKpmG)$cUvm#kuJ5a&n7 zKwoed`cbiiV36q;%kHablMYUkjhwVEe>ni9NAEKCDssFYr}r#<_3wP>>Hd{`Q5 z^v@OKNI9NpWXv+6_E5v!OkWZHe0g*ojQnIdQ>nbu1%`agLReO7Y&}63LUv+Q<>1vX zB3%no$|uSW4{%i#E;1V%4F72fqoz<6s<9WxIl109HL^Z-pkHUIxWFw`uTH=9X4&O4 zMbct@!1-~#+i2dL)BVXyxjF{g-z(?bR@O<9>wf7mq70~u8w!CEJqHG~c%CZW`(%kd zs3o8KdFKvNBDdK#>vv!L+xdFR{Oq=>)@tI^a&zQISk+&3aHwkNS4IANHa$6lq=R|LWSP2faD%^?v8;1PCrSOO{H4$_0K_1TlN)2eS)1Itf0??`RGXT8;3>-24}R; z9J`<6jR9r}K^W@V*_4<)P>+Ot%MYx1keP?uY?$=_Nc_L+`@j3sL3~~3IbXD0p;)!( zLtxKORtUCmM8VHUNACZe0n}jjSPq&Gj#{fICQ{2_7#yR|Kv@`$tLIf9q&nqO<$|9{s=i4nkfC{~Gr3ilhTP zS60G8-fU5xrb!TS`OvttNL?+3zWk#}Kp2p$e0Q$$2x>5N)T+6(N*+bQB)X+{> zTRPd)%TTxgjX2});5ao8@?>liKnv@h)TDV24N4MD*c`aw!|w&$8tPOt;2u?e`|E0d z+>=}4xcx2Dr0$_Bmaiiq1MebgZ5GpBHTu6gX4xhVh)ivskf!l6WBPj>@HA099I|fH z#BVJ>7DoG+tp#KHXLsta717CIv{XAqC;{To?2HPc+zwmSK0E4>Jrg+AI0D96Gr|(J@QdLs_?h{efy$tZ(hie@&a&Xx%@e1|5r7= zoT%ERREWb(K-H6&rA_nMs@O_7Ip7F-=H6O0(V4$pj#`L3MPLa&XbHT`q-j^s;aA6z zG9nM{70z-6hB$r#WMRE!W^H~663nAQKBTTx@_b_SShUopSevzH@T$`&1YyR;O}C)ja>=8xQ{ zXsJkJoZWim-C~K)^sXqV(`Y|GCFolz?7XFv%4Qr1AYZY;v^$2+3}dx&yiv&?bhd{| zUOa5YOGmjh&NMKi%B^C=3&b|cYD(1x@(>ZV`8Erx+C0*4 z19+%ef29w*d(8UcZ_k~qh?W-5&j26IlXZTA*Vj&SH78Au^&Ks;of{17WMCTs*>s~FfbDm?S*h=7XYf1|8(B5nL$ufe} zs%h}O@UCwiXB!=_;)E_yu|ne>OOiHgqej#Vec^EGSv_hF_(QUu7X|Gmpr#*Do6V&U zL&kx%Fd){>u3Wa$)pgS}SF$piM&Cwv0=K?E?dtRVF0KD~XGxQmCUfw~4*IbDu+33f z%d~K0Vy_1CC|55W0_Ol^PLC@GjKum_)33u(i|%E*O9Ql2fts#?s{S<~Jy~QY@N)CG z_go;xO;yWNYQVHjJ|{04(A6=W;MD|3FOYMfJ$BFxu8CKYL=xz4MySGvH+Lx8WzTkE zv1CQkeq{r=p>aX>^wnF2n^=F(a=b$O=OWHB>} z$8Y0g0vn`vay-`YNTqxF?4YsI%t4VhY*UF>g{0QsG9StmDLK0ngVSm|=rF*qq`R6O zgoXzg2Ok#(8x58+*``iJ1~MKPOm+DMoh-63obSn=Ydbeg*rHJOsr*I%mlYw1_O&me;$!hIdBf$mFHJ5yn(!Q5TK<1NR8h`4?%$8d%aEA}NeAm4H zOfHRScI@4pYF|RA^sSvpH0_TXhc(l$Z0tn^-mNn!aut<14>0$YjsbMe%SnoX=tY_7 zGS9jB@SFlK^J`wi}$orsT+J4T&q8jZ&R6;_AuGZZw(5#AqQMuQgy5TK$z6h zeKcLx_6PTX1_L(QV$GSy8e4H;zZ>?IfQz_#2XmiAsEU+GaQJNTKu(yk*Bvb}0 z^DLA8fq#%boc6GllzK^PWGfs75G88t}Q#f-?23@9};OERiUA{~%JK{^E9{2r?x7ZBpJL9vr zl$eJO{cS7Llt=SOyP3n@3&((46Ur|ufjVgZ)Pog{_-YA56Ma&G=18;pGOx$z#9y*j zR^Pxh+`s?xSFZKhZyM#a47cvPOdL0T*rq-=%7t>F0^BzQ;_uwSamf;UciYt9f?db5 z2i38<9xG$dP)-sNPmSF}KPdg^@qp+)wR~{C39}t4l8(qZ_PR91G4$<8ox{}OI6Yzc zxQJ`u=EnJ!Yq0S^s2Px!!Ap{bPX_6QA z7A3zu*<)D?%d=^-x&(?2S%M3KLP8L>S~dx9+WweFs~`$ebcK_XbcKb5&Hm;vNEeT+ zo#+Y|+dQf)=?&Qc;z3`YPKSL7LgAmcV^4c;DNjl*-o_*+26j+8k1}T%cw%--n;)C${D9KZifoA9cAh9+o~T+ z>VZEVAl^|d3&DQa`$~$DIbL-)3u>o7HW7@?qsE6j#0Vbd^9L52h~Cj^t_YG2I37Vg znp1axDtAzTcM`#{Q=$+^tzA4&DC^;_ww#e1+568nudZz=cV; zZ7nl={Wq(X#Aoz!`0Gr$<^Z2+*Kpo(et7<9?C#jr-n%9Iip9;uZNTNX`b>^wiFoij z^T^U1gqm-rZ?bgpFNKeIgqpUJ}1TV&>>MYK7kv zi6E#m3!+Rvi57DWj5i2fQ|VJd042%rd*e30ea^EZ9aEi+fAW+}pA1!_+$zK?xH~w)@kaA(m#B4y3cJ$ok78>X%1P=sEYHBqHZ8&^#Ngx!h5V`*7Hzk zyj?UO-uWCF?M55vKf9NOlk{8o=Kr(v_1RPNU<2Bn$Fe^j_wtwg9uQT&vh$lY;u7L6 z^5<~CHO)on?lC~+5t2#md0tjUd*7~@w>`HVaw+<^zAx$}0ZgN7d} zGny10279N(_}5Xxwju5QzEnR-E1F9fr+!w=?VXcY@w}PX4}}FFwNe=1S%Ady zdd1Y%K~}d=JWMEl`VM&igPN3vYscyD3`eQ59{;3VdsHL@OU0F;p=X|NCl8VdDPb7I zS!QmuKD07Rxgu}8luQ^qWDP)=CwABTz zJa7{!Ywdegr%oA@YULW$+BKS}Z)O-xEi#8P&pI;hC-?@PlsWCM<8Vl!=D$p~VU9Y0 zV+YC!qZ^!qzan2;;$-Rlv`X~^@sEzf1I?%DdUdDrA^7VqpRvNaCb{J$-0#5f+sbDe zA7d{&)+OwZpw8-;6?1aSA1;TQzfQij;`J)@H%=9_APcGEZ z)99-R?lc`;``6$P;Af}-;+*x zV}BQy71G{y--srWy?tpe2SuEHiT!ToC0x$ldg1U@XUAuVf($^GQQtszlQ5*w-2%#9 z1J2pn48V8Dra)~JK_Nx@#tJ(A##f_!D{__4MSTogx(aJ-@}4rv!twp#msPbF$OQ&8 z3=#uF)^pr(ICN^*)zS(X6;#Ic5Ru6rN=0YQd&CDyjbbciWII|-JLPck6M6yap~a&U zSk&nRD)G7UPX#3^=2%@3VHKc6aOflFslS~URn>BSU8Akjv2pHn;tlb^x`RkDLAVR> zDn2AyY7EklgQU$*WG@C$V?*R`V}4Dc`k2CLt7T6mT<-bO>0Y;s8g_n(nnI*Na_O6; zn?>9Pp9)l)uNA8Yu4|)vxlkra{iR$#uW-oY_x3QQp1Z`qDm1~Qskw@75!K;=+xg<< zHO1maLFYR##7g47chr?CvYSmQZNG9cv*eQ+ZC(Fj{a62_^wz)q-W{YFN9X55KLb+; zCB;#J6}d}8jqUs80!`0$KMcz@!EEItYG5Wfg@uooUuZ)gt_%g4AoadGD;rC1zoRYd zj$Fv_7@XjKqA-l#L!Ac**9#p?*N!!@_4GXv<-ispVEBu_na5OCsYgb~1A>D532olu z*3ubc1xt5H%xyVZHC51e5)8hH10GoEM%Q7@l3I@oaKyhxI}>#;;P!@0MNOF1Lx(L}}>^eY>zBd3LmD*MN?b z$7v;sT(85^M|2&_fJ{{W{<>DdG5O5tIs5KrE%wozzPjxI9$$Foi%JF?nd~BTePE)_;8rkHX}xU z!LI3Z^W^<7W&w{+nx=^Chs-0$S&LMdOn-h`g$SUx8~GVIZ?qmEp!3-C6D;7-t7VBb z8MWiC2-!`+-c;REJzW(rnp$*PBRV=XhB2p(jw)%K{&|6T3~{~9qaj9YAv`tk$6#qjQ`S1_((Y*?l7SG_ecA~~Bm47NEBG#zbpz(W3I)H~u(cQPTOu;g%Gn&Kknix6 zeond)d?uAGDiIe}SYJKZ*En5x7!roTN=0U!5fgD5bK=wq_)_?lP zyCXGh+oFoM=ZpJtpL?*l-stn3O)_H8|NL}7bk5`o{`a7FyT_c!(0Zsi0}vGDJin3) zVw0?W*=4ADv6*446^UFk{ke|6U-{6mk@Tm9!TU=mOCQ9rEa5c!KIsyTShiF_Kp^wj z3&|~=HvhUnd|*ZhkZsjv3}R^`geBB}PqNvXdUkI1WIM%sVEnDv#JPz3bz^4!x5HTd z>^Js%B^t*qqbvu~garv!!Mj`ihxjnDO6Tt(9?43@(YT2?)nm@_` z8zlsvjs<5HhcXVI+W>{nfLjmCVK>>uf0V#!s4PxEUu;GX`n-rt@XH;x{<2wvX~q>n z0E{NZDh680kN=-{J!;up5KMrmJt=kk9~hb%h!FpQlmCA|+BEWYkd{P807WPk?xqD4 zp*P6zRwJY70U1JBU^|;6d&BJ1+eGa^|@EirtJyk0Y&V~#P9fq^GI%-R{A zqlP#g_2UizaHyNGNj43zMdU$kBWimO!+HPw_yny3_psmtDhl=mnT&z_SKI9y1;dTo z9mlxJ67T4hpU-J$R^Pcm<1RlSdJ~tE^*xRs7qz(FmF zFYD}YVKOQTKy@JlqI7l%5g!J)v^DzhPKG843n$>c_@9Q4#73|C=SDpoBs#}G_E(9- zCuuWE#`<(U=o_XyUGT;Fo7MldCu1l2Rw_}}zX-wSBuB@`%DvSm@k*?Q8Lz)n**F7= zj4Qov)zAwk3xY8eILq^zj~@mWwOVBv{u=RYSU{VWi^Z{NO?S- z*JRU4dh&1DmKPMN^nOnlI@|kcd9#Mfaq-hZT@fQJLQq+U`oW~alx@08B)QgPLViE) z({I;0A*Z;JGv0G;ODJ?jZdsoxiuPdZMgI`~Q9X- zg*JbkYPXuOfv*g;V(g8wyru?a&rcSj%ug)ErS0d$>`*{8-M1BTIt2uPsMEu_;FXi* zsU@i=sJ%;r-We?>0rW&CW}YcuAm`L=J!~WJVc9BW@#5Ij33?RCPG&DP)r0giOr~xe z$kQ07Tub>e&8+7~JyTcG>pqQlr@E87Y9j+6yM&OEK-Z0E`d_q8D&c|r=qdl)a@++& zh6tvd^5}9>!H?1aM4VOH{JrW|qeGC!g1Vg4HX^OU57F*wlcP@}4V0#cedzXE zYWOJMLqatD;ftXLSGTOtnTn*VRCFL&0DUuaTZq{3ScW8={UMxHsA|L;tc5*{;5|$m zYqc%#_y=VE4l;b*Z2NtxZ&`Xnx7pcla&p9H=1P`;?QhFYWpt33l!7PcM8z7%xxhod zgiuZD#8c;~StXg>_yr)yJMO%r-S&RVI>^}sh||F3paLaU2wwg{(+}nME&sbw* z1+MTbe{;_ud=;6%is9v_@Oc!wr-a}R)w;h?%y2e@9Ym8m{zGo8Of&D+ez$0QwQ7at z@k8Zm&B;$Yei2ZF6zcd_Vc!^c8=AC=Ms#84!`RFMHjL*UnoI8W3u19RC9!(iK0Ggh%%IC`1Qs?I)}N#(G2V}>-uUFSm%eqXq61|jIM1rxX; z3`DyL)`*(8)->+kAsJfxcJwe(vG(?dJ{Q|sJNmSsc(

nhVj9ZIkrJQ)Wy{s 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 + +Before reading this section, read the tutorials [Loss Function](https://www.mindspore.cn/tutorials/en/master/advanced/modules/loss.html) on the MindSpore official website first. + +## Network Basic Unit: Cell + +MindSpore uses [Cell](https://www.mindspore.cn/docs/en/master/api_python/nn/mindspore.nn.Cell.html#mindspore.nn.Cell) to construct graphs. You need to define a class that inherits the `Cell` base class, declare the required APIs and submodules in `init`, and perform calculation in `construct`. `Cell` compiles a computational graph in `GRAPH_MODE` (static graph mode). It is used as the basic module of neural network in `PYNATIVE_MODE` (dynamic graph mode). The basic `Cell` setup process is as follows: + +```python +import mindspore.nn as nn +import mindspore.ops as ops + +class MyCell(nn.Cell): + def __init__(self, forward_net): + super(MyCell, self).__init__(auto_prefix=True) + self.net = forward_net + self.relu = ops.ReLU() + + def construct(self, x): + y = self.net(x) + return self.relu(y) + +inner_net = nn.Conv2d(120, 240, 4, has_bias=False) +my_net = MyCell(inner_net) +print(my_net.trainable_params()) +``` + +```text + [Parameter (name=net.weight, shape=(240, 120, 4, 4), dtype=Float32, requires_grad=True)] +``` + +A parameter name is generally formed based on an object name defined by `__init__` and a name used during parameter definition. For example, in the foregoing example, a convolutional parameter name is `net.weight`, where `net` is an object name in `self.net = forward_net`, and `weight` is `name`: `self.weight = Parameter(initializer(self.weight_init, shape), name='weight')` when a convolutional parameter is defined in Conv2d. + +To align parameter names, you may not need to add object names. The cell provides the `auto_prefix` interface to determine whether to add object names to parameter names in the cell. The default value is `True`, that is, add object names. If `auto_prefix` is set to `False`, the `name` of `Parameter` in the preceding example is `weight`. + +### Unit Test + +After the `Cell` is set up, you are advised to build a unit test method for each `Cell` and compare it with the benchmarking code. In the preceding example, the PyTorch build code is as follows: + +```python +import torch.nn as torch_nn + +class MyCell_pt(torch_nn.Module): + def __init__(self, forward_net): + super(MyCell_pt, self).__init__() + self.net = forward_net + self.relu = torch_nn.ReLU() + + def forward(self, x): + y = self.net(x) + return self.relu(y) + +inner_net_pt = torch_nn.Conv2d(120, 240, kernel_size=4, bias=False) +pt_net = MyCell_pt(inner_net_pt) +for i in pt_net.parameters(): + print(i.shape) +``` + +```text + torch.Size([240, 120, 4, 4]) +``` + +With the script for building the `Cell`, you need to use the same input data and parameters to compare the output. + +```python +import numpy as np +import mindspore as ms +import torch + +x = np.random.uniform(-1, 1, (2, 120, 12, 12)).astype(np.float32) +for m in pt_net.modules(): + if isinstance(m, torch_nn.Conv2d): + torch_nn.init.constant_(m.weight, 0.1) + +for _, cell in my_net.cells_and_names(): + if isinstance(cell, nn.Conv2d): + cell.weight.set_data(ms.common.initializer.initializer(0.1, cell.weight.shape, cell.weight.dtype)) + +y_ms = my_net(ms.Tensor(x)) +y_pt = pt_net(torch.from_numpy(x)) +diff = np.max(np.abs(y_ms.asnumpy() - y_pt.detach().numpy())) +print(diff) + +# ValueError: operands could not be broadcast together with shapes (2,240,12,12) (2,240,9,9) +``` + +The output of MindSpore is different from that of PyTorch. Why? + +According to the [Function Differences with torch.nn.Conv2d](https://www.mindspore.cn/docs/en/master/note/api_mapping/pytorch_diff/nn_Conv2d.html), the default parameters of `Conv2d` are different in MindSpore and PyTorch. +By default, MindSpore uses the `same` mode, and PyTorch uses the `pad` mode. During migration, you need to modify the `pad_mode` of MindSpore `Conv2d`. + +```python +import numpy as np +import mindspore as ms +import torch + +inner_net = nn.Conv2d(120, 240, 4, has_bias=False, pad_mode="pad") +my_net = MyCell(inner_net) + +# Construct random input. +x = np.random.uniform(-1, 1, (2, 120, 12, 12)).astype(np.float32) +for m in pt_net.modules(): + if isinstance(m, torch_nn.Conv2d): + # Fixed PyTorch initialization parameter + torch_nn.init.constant_(m.weight, 0.1) + +for _, cell in my_net.cells_and_names(): + if isinstance(cell, nn.Conv2d): + # Fixed MindSpore initialization parameter + cell.weight.set_data(ms.common.initializer.initializer(0.1, cell.weight.shape, cell.weight.dtype)) + +y_ms = my_net(ms.Tensor(x)) +y_pt = pt_net(torch.from_numpy(x)) +diff = np.max(np.abs(y_ms.asnumpy() - y_pt.detach().numpy())) +print(diff) +``` + +```text + 2.9355288e-06 +``` + +The overall error is about 0.01%, which basically meets the expectation. **During cell migration, you are advised to perform a unit test on each cell to ensure migration consistency.** + +### Common Methods of Cells + +`Cell` is the basic unit of the neural network in MindSpore. It provides many flag setting and easy-to-use methods. The following describes some common methods. + +#### Manual Mixed-precision + +MindSpore provides an auto mixed precision method. For details, see the amp_level attribute in [Model](https://www.mindspore.cn/docs/en/master/api_python/train/mindspore.train.Model.html#mindspore.train.Model). + +However, sometimes the hybrid precision policy is expected to be more flexible during network development. MindSpore also provides the [to_float](https://mindspore.cn/docs/en/master/api_python/nn/mindspore.nn.Cell.html#mindspore.nn.Cell.to_float) method to manually add hybrid precision. + +`to_float(dst_type)`: adds type conversion to the input of the `Cell` and all child `Cell` to run with a specific floating-point type. + +If `dst_type` is `ms.float16`, all inputs of `Cell` (including input, `Parameter`, and `Tensor` used as constants) will be converted to `float16`. For example, if you want to change all BNs and losses in ResNet to the `float32` type and other operations to the `float16` type, run the following command: + +```python +import mindspore as ms +from mindspore import nn +from mindvision.classification.models import resnet50 + +resnet = resnet50(pretrained=False) +resnet.to_float(ms.float16) #Add the float16 flag to all operations in the net. The framework adds the cast method to the input during compilation. +for _, cell in resnet.cells_and_names(): + if isinstance(cell, (nn.BatchNorm1d, nn.BatchNorm2d, nn.BatchNorm3d)): + cell.to_float(ms.float32) + +loss = nn.SoftmaxCrossEntropyWithLogits(sparse=True, reduction='mean').to_float(ms.float32) +net_with_loss = nn.WithLossCell(resnet, loss_fn=loss) +``` + +The customized `to_float` conflicts with the `amp_level` in the model. If the customized mixing precision is used, do not set the `amp_level` in the model. + +#### Customizing Initialization Parameters + +Generally, the high-level API encapsulated by MindSpore initializes parameters by default. Sometimes, the initialization distribution is inconsistent with the required initialization and PyTorch initialization. In this case, you need to customize initialization. [Initializing Network Arguments](https://mindspore.cn/tutorials/en/master/advanced/modules/parameter.html#initializing-network-arguments) describes a method of initializing parameters by using API attributes. This section describes a method of initializing parameters by using Cell. + +For details about the parameters, see [Network Parameters](https://www.mindspore.cn/tutorials/en/master/advanced/modules/parameter.html). This section uses `Cell` as an example to describe how to obtain all parameters in `Cell` and how to initialize the parameters in `Cell`. + +> Note that the method described in this section cannot be performed in `construct`. To change the value of a parameter on the network, use [assign](https://www.mindspore.cn/docs/en/master/api_python/ops/mindspore.ops.assign.html). + +[set_data(data, slice_shape=False)](https://www.mindspore.cn/docs/en/master/api_python/mindspore/mindspore.Parameter.html?highlight=set_data#mindspore.Parameter.set_data) sets parameter data. + +For details about the parameter initialization methods supported by MindSpore, see [mindspore.common.initializer](https://www.mindspore.cn/docs/en/master/api_python/mindspore.common.initializer.html). You can also directly transfer a defined [Parameter](https://www.mindspore.cn/docs/en/master/api_python/mindspore/mindspore.Parameter.html#mindspore.Parameter) object. + +```python +import math +import mindspore as ms +from mindspore import nn +from mindvision.classification.models import resnet50 + +resnet = resnet50(pretrained=False) +for _, cell in resnet.cells_and_names(): + if isinstance(cell, nn.Conv2d): + cell.weight.set_data(ms.common.initializer.initializer( + ms.common.initializer.HeNormal(negative_slope=0, mode='fan_out', nonlinearity='relu'), + cell.weight.shape, cell.weight.dtype)) + elif isinstance(cell, (nn.BatchNorm2d, nn.GroupNorm)): + cell.gamma.set_data(ms.common.initializer.initializer("ones", cell.gamma.shape, cell.gamma.dtype)) + cell.beta.set_data(ms.common.initializer.initializer("zeros", cell.beta.shape, cell.beta.dtype)) + elif isinstance(cell, (nn.Dense)): + cell.weight.set_data(ms.common.initializer.initializer( + ms.common.initializer.HeUniform(negative_slope=math.sqrt(5)), + cell.weight.shape, cell.weight.dtype)) + cell.bias.set_data(ms.common.initializer.initializer("zeros", cell.bias.shape, cell.bias.dtype)) +``` + +#### Freezing Parameters + +`Parameter` has a `requires_grad` attribute to determine whether to update parameters. When `requires_grad=False`, `Parameter` is equivalent to the `buffer` object of PyTorch. + +You can obtain the parameter list in `Cell` through `parameters_dict`, `get_parameters`, and `trainable_params` of the cell. + +- parameters_dict: obtains all parameters in the network structure and returns an `OrderedDict` with `key` as the parameter name and `value` as the parameter value. + +- get_parameters: obtains all parameters in the network structure and returns the iterator of the `Parameter` in the `Cell`. + +- trainable_params: obtains the attributes whose `requires_grad` is `True` in `Parameter` and returns the list of trainable parameters. + +```python +import mindspore.nn as nn + +net = nn.Dense(2, 1, has_bias=True) +print(net.trainable_params()) + +for param in net.trainable_params(): + param_name = param.name + if "bias" in param_name: + param.requires_grad = False +print(net.trainable_params()) +``` + +```text + [Parameter (name=weight, shape=(1, 2), dtype=Float32, requires_grad=True), Parameter (name=bias, shape=(1,), dtype=Float32, requires_grad=True)] + [Parameter (name=weight, shape=(1, 2), dtype=Float32, requires_grad=True)] +``` + +When defining an optimizer, use `net.trainable_params()` to obtain the list of parameters that need to be updated. + +In addition to setting the parameter `requires_grad=False` not to update the parameter, you can also use `stop_gradient` to block gradient calculation to freeze the parameter. When will `requires_grad=False` and `stop_gradient` be used? + +![parameter-freeze](https://mindspore-website.obs.cn-north-4.myhuaweicloud.com/website-images/master/docs/mindspore/source_en/migration_guide/model_development/images/parameter_freeze.png) + +As shown in the preceding figure, the `requires_grad=False` does not update some parameters, but the backward gradient calculation is normal. +The `stop_gradient` directly performs backward gradient. When there is no parameter to be trained before the parameter to be frozen, the two parameters are equivalent in function. +However, `stop_gradient` is faster (with less backward gradient calculations). +If there are parameters to be trained before the frozen parameters, only `requires_grad=False` can be used. + +#### Saving and Loading Parameters + +MindSpore provides the `load_checkpoint` and `save_checkpoint` methods for saving and loading parameters. Note that when a parameter is saved, the parameter list is saved. When a parameter is loaded, the object must be a cell. +When loading parameters, you may need to modify the parameter names. In this case, you can directly construct a new parameter list for the `load_checkpoint` to load the parameter list to the cell. + +```python +import mindspore as ms +import mindspore.ops as ops +import mindspore.nn as nn + +net = nn.Dense(2, 1, has_bias=True) +for param in net.get_parameters(): + print(param.name, param.data.asnumpy()) + +ms.save_checkpoint(net, "dense.ckpt") +dense_params = ms.load_checkpoint("dense.ckpt") +print(dense_params) +new_params = {} +for param_name in dense_params: + print(param_name, dense_params[param_name].data.asnumpy()) + new_params[param_name] = ms.Parameter(ops.ones_like(dense_params[param_name].data), name=param_name) + +ms.load_param_into_net(net, new_params) +for param in net.get_parameters(): + print(param.name, param.data.asnumpy()) +``` + +```text + weight [[-0.0042482 -0.00427286]] + bias [0.] + {'weight': Parameter (name=weight, shape=(1, 2), dtype=Float32, requires_grad=True), 'bias': Parameter (name=bias, shape=(1,), dtype=Float32, requires_grad=True)} + weight [[-0.0042482 -0.00427286]] + bias [0.] + weight [[1. 1.]] + bias [1.] +``` + +### Dynamic and Static Graphs + +For `Cell`, MindSpore provides two image modes: `GRAPH_MODE` (static image) and `PYNATIVE_MODE` (dynamic image). For details, see [Dynamic Image and Static Graphs](https://www.mindspore.cn/tutorials/en/master/advanced/compute_graph.html). + +The **inference** behavior of the model in `PyNative` mode is the same as that of common Python code. However, during training, **once a tensor is converted into NumPy for other operations, the gradient of the network is truncated, which is equivalent to detach of PyTorch**. + +When `GRAPH_MODE` is used or `PYNATIVE_MODE` is used for **training**, syntax restrictions usually occur. In these two cases, graph compilation needs to be performed on the Python code. However, MindSpore does not support the complete Python syntax set. Therefore, there are some restrictions on compiling the `construct` function. For details about the restrictions, see [MindSpore Static Graph Syntax](https://www.mindspore.cn/docs/en/master/note/static_graph_syntax_support.html). + +#### Common Restrictions + +Compared with the detailed syntax description, the common restrictions are as follows: + +- Scenario 1 + + Restriction: During image composition (construct functions or functions modified by ms_function), do not invoke other Python libraries, such as NumPy and scipy. Related processing must be moved forward to the `__init__` phase. + Measure: Use the APIs provided by MindSpore to replace the functions of other Python libraries. The processing of constants can be moved forward to the `__init__` phase. + +- Scenario 2 + + Restriction: Do not use user-defined types during graph build. Instead, use the data types provided by MindSpore and basic Python types. You can use the tuple/list combination based on these types. + Measure: Use basic types for combination. You can increase the number of function parameters. There is no restriction on the input parameters of the function, and variable-length input can be used. + +- Scenario 3 + + Restriction: Do not perform multi-thread or multi-process processing on data during image composition. + Measure: Avoid multi-thread processing on the network. + +### Customized Backward Network Construction + +Sometimes, MindSpore does not support some processing and needs to use some third-party library methods. However, we do not want to truncate the network gradient. In this case, what should we do? This section describes how to customize backward network construction to avoid this problem in `PYNATIVE_MODE`. + +In this scenario, a value greater than 0.5 needs to be randomly selected, and the shape of each batch is fixed to `max_num`. However, the random put-back operation is not supported by MindSpore APIs. In this case, NumPy is used for computation in `PYNATIVE_MODE`, and then a gradient propagation process is constructed. + +```python +import numpy as np +import mindspore as ms +import mindspore.nn as nn +import mindspore.ops as ops + +ms.set_context(mode=ms.PYNATIVE_MODE) +ms.set_seed(1) + +class MySampler(nn.Cell): + # Customize a sampler and select `max_num` values greater than 0.5 in each batch. + def __init__(self, max_num): + super(MySampler, self).__init__() + self.max_num = max_num + + def random_positive(self, x): + # Method of the third-party library NumPy. Select a position greater than 0.5. + pos = np.where(x > 0.5)[0] + pos_indice = np.random.choice(pos, self.max_num) + return pos_indice + + def construct(self, x): + # Forward Network Construction + batch = x.shape[0] + pos_value = [] + pos_indice = [] + for i in range(batch): + a = x[i].asnumpy() + pos_ind = self.random_positive(a) + pos_value.append(ms.Tensor(a[pos_ind], ms.float32)) + pos_indice.append(ms.Tensor(pos_ind, ms.int32)) + pos_values = ops.stack(pos_value, axis=0) + pos_indices = ops.stack(pos_indice, axis=0) + print("pos_values forword", pos_values) + print("pos_indices forword", pos_indices) + return pos_values, pos_indices + +x = ms.Tensor(np.random.uniform(0, 3, (2, 5)), ms.float32) +print("x", x) +sampler = MySampler(3) +pos_values, pos_indices = sampler(x) +grad = ops.GradOperation(get_all=True)(sampler)(x) +print("dx", grad) +``` + +```text + x [[1.2510660e+00 2.1609735e+00 3.4312444e-04 9.0699774e-01 4.4026768e-01] + [2.7701578e-01 5.5878061e-01 1.0366821e+00 1.1903024e+00 1.6164502e+00]] + pos_values forword [[0.90699774 2.1609735 0.90699774] + [0.5587806 1.6164502 0.5587806 ]] + pos_indices forword [[3 1 3] + [1 4 1]] + pos_values forword [[0.90699774 1.251066 2.1609735 ] + [1.1903024 1.1903024 0.5587806 ]] + pos_indices forword [[3 0 1] + [3 3 1]] + dx (Tensor(shape=[2, 5], dtype=Float32, value= + [[0.00000000e+000, 0.00000000e+000, 0.00000000e+000, 0.00000000e+000, 0.00000000e+000], + [0.00000000e+000, 0.00000000e+000, 0.00000000e+000, 0.00000000e+000, 0.00000000e+000]]),) +``` + +When we do not construct this backward process, the gradient will be truncated because the numpy method is used to calculate the `pos_value`. +As shown in the preceding information, the value of `dx` is all 0s. In addition, you may find that `pos_values forword` and `pos_indices forword` are printed twice in this process. This is because the forward graph is constructed again when the backward graph is constructed in `PYNATIVE_MODE`. As a result, the forward graph is constructed twice and the backward graph is constructed once, which wastes training resources. In some cases, precision problems may occur. For example, in the case of BatchNorm, `moving_mean` and `moving_var` are updated during forward running. As a result, `moving_mean` and `moving_var` are updated twice during one training. +To avoid this scenario, MindSpore has a method `set_grad()` for `Cell`. In `PYNATIVE_MODE` mode, the framework synchronously constructs the backward process when constructing the forward process. In this way, the forward process is not executed when the backward process is executed. + +```python +x = ms.Tensor(np.random.uniform(0, 3, (2, 5)), ms.float32) +print("x", x) +sampler = MySampler(3).set_grad() +pos_values, pos_indices = sampler(x) +grad = ops.GradOperation(get_all=True)(sampler)(x) +print("dx", grad) +``` + +```text + x [[1.2519144 1.6760695 0.42116082 0.59430444 2.4022336 ] + [2.9047847 0.9402725 2.076968 2.6291676 2.68382 ]] + pos_values forword [[1.2519144 1.2519144 1.6760695] + [2.6291676 2.076968 0.9402725]] + pos_indices forword [[0 0 1] + [3 2 1]] + dx (Tensor(shape=[2, 5], dtype=Float32, value= + [[0.00000000e+000, 0.00000000e+000, 0.00000000e+000, 0.00000000e+000, 0.00000000e+000], + [0.00000000e+000, 0.00000000e+000, 0.00000000e+000, 0.00000000e+000, 0.00000000e+000]]),) +``` + +Now, let's see how to [customize backward network construction](https://www.mindspore.cn/tutorials/experts/en/master/network/custom_cell_reverse.html). + +```python +import numpy as np +import mindspore as ms +import mindspore.nn as nn +import mindspore.ops as ops + +ms.set_context(mode=ms.PYNATIVE_MODE) +ms.set_seed(1) + +class MySampler(nn.Cell): + # Customize a sampler and select `max_num` values greater than 0.5 in each batch. + def __init__(self, max_num): + super(MySampler, self).__init__() + self.max_num = max_num + + def random_positive(self, x): + # Method of the third-party library NumPy. Select a position greater than 0.5. + pos = np.where(x > 0.5)[0] + pos_indice = np.random.choice(pos, self.max_num) + return pos_indice + + def construct(self, x): + # Forward network construction + batch = x.shape[0] + pos_value = [] + pos_indice = [] + for i in range(batch): + a = x[i].asnumpy() + pos_ind = self.random_positive(a) + pos_value.append(ms.Tensor(a[pos_ind], ms.float32)) + pos_indice.append(ms.Tensor(pos_ind, ms.int32)) + pos_values = ops.stack(pos_value, axis=0) + pos_indices = ops.stack(pos_indice, axis=0) + print("pos_values forword", pos_values) + print("pos_indices forword", pos_indices) + return pos_values, pos_indices + + def bprop(self, x, out, dout): + # Backward network construction + pos_indices = out[1] + print("pos_indices backward", pos_indices) + grad_x = dout[0] + print("grad_x backward", grad_x) + batch = x.shape[0] + dx = [] + for i in range(batch): + dx.append(ops.UnsortedSegmentSum()(grad_x[i], pos_indices[i], x.shape[1])) + return ops.stack(dx, axis=0) + +x = ms.Tensor(np.random.uniform(0, 3, (2, 5)), ms.float32) +print("x", x) +sampler = MySampler(3).set_grad() +pos_values, pos_indices = sampler(x) +grad = ops.GradOperation(get_all=True)(sampler)(x) +print("dx", grad) +``` + +```text + x [[1.2510660e+00 2.1609735e+00 3.4312444e-04 9.0699774e-01 4.4026768e-01] + [2.7701578e-01 5.5878061e-01 1.0366821e+00 1.1903024e+00 1.6164502e+00]] + pos_values forword [[0.90699774 2.1609735 0.90699774] + [0.5587806 1.6164502 0.5587806 ]] + pos_indices forword [[3 1 3] + [1 4 1]] + pos_indices backward [[3 1 3] + [1 4 1]] + grad_x backward [[1. 1. 1.] + [1. 1. 1.]] + dx (Tensor(shape=[2, 5], dtype=Float32, value= + [[0.00000000e+000, 1.00000000e+000, 0.00000000e+000, 2.00000000e+000, 0.00000000e+000], + [0.00000000e+000, 2.00000000e+000, 0.00000000e+000, 0.00000000e+000, 1.00000000e+000]]),) +``` + +The `bprop` method is added to the `MySampler` class. The input of this method is forward input (expanded write), forward output (a tuple), and output gradient (a tuple). In this method, a gradient-to-input backward propagation process is constructed. +In batch 0, the values at positions 3, 1, and 3 are randomly selected. The output gradient is 1, and the reverse gradient is `[0.00000000e+000, 1.00000000e+000, 0.00000000e+000, 2.00000000e+000, 0.00000000e+000]`, which meets the expectation. + +### Dynamic Shape Workarounds + +Generally, dynamic shape is introduced due to the following reasons: + +- The input shape is not fixed. +- Operators that cause shape changes exist during network execution. +- Different branches of the control flow introduce shape changes. + +Now, let's look at some workarounds for these scenarios. + +#### Input Shape Not Fixed + +1. You can add pads to the input data to a fixed shape. For example, [Data Processing](https://gitee.com/mindspore/models/blob/master/research/audio/deepspeech2/src/dataset.py#L153) of deep_speechv2 specifies the maximum length of `input_length`. Short `input_length` are padded with 0s, and long `input_length` are randomly truncated. Note that this method may affect the training accuracy. Therefore, the training accuracy and training performance need to be balanced. + +2. You can set a group of fixed input shapes to process the input into several fixed scales. For example, in [Data Processing](https://gitee.com/mindspore/models/blob/master/official/cv/yolov3_darknet53/src/yolo_dataset.py#L177) of YOLOv3_darknet53, the processing function `multi_scale_trans` is added to the batch method, and a shape is randomly selected from [MultiScaleTrans](https://gitee.com/mindspore/models/blob/master/official/cv/yolov3_darknet53/src/transforms.py#L456) for processing. + +Currently, the support for completely random input shapes is limited and needs to be supported in the new version. + +#### Operations that Cause Shape Changes During Network Execution + +In the scenario where tensors with unfixed shapes are generated during network running, the most common method is to construct a mask to filter out values in invalid positions. For example, in the detection scenario, some boxes need to be selected based on the iou results of the prediction box and real box. +The PyTorch implementation is as follows: + +```python +def box_select_torch(box, iou_score): + mask = iou_score > 0.3 + return box[mask] +``` + +In versions later than MindSpore 1.8, `masked_select` is supported in all scenarios. In MindSpore, `masked_select` can be implemented as follows: + +```python +import mindspore as ms +from mindspore import ops + +ms.set_seed(1) + +def box_select_ms(box, iou_score): + mask = (iou_score > 0.3).expand_dims(1) + return ops.masked_select(box, mask) +``` + +Let's look at the result comparison. + +```python +import torch +import numpy as np +import mindspore as ms + +ms.set_seed(1) + +box = np.random.uniform(0, 1, (3, 4)).astype(np.float32) +iou_score = np.random.uniform(0, 1, (3,)).astype(np.float32) + +print("box_select_ms", box_select_ms(ms.Tensor(box), ms.Tensor(iou_score))) +print("box_select_torch", box_select_torch(torch.from_numpy(box), torch.from_numpy(iou_score))) +``` + +```text + box_select_ms [0.14675589 0.09233859 0.18626021 0.34556073] + box_select_torch tensor([[0.1468, 0.0923, 0.1863, 0.3456]]) +``` + +However, after this operation, dynamic shape is generated, which may cause problems in subsequent network calculation. Currently, you are advised to use the mask to avoid this problem. + +```python +def box_select_ms2(box, iou_score): + mask = (iou_score > 0.3).expand_dims(1) + return box * mask, mask +``` + +In subsequent computation, if some box operations are involved, check whether the mask needs to be multiplied to filter invalid results. + +If a tensor with an unfixed shape is obtained due to feature selection during loss computation, the processing method is basically the same as that during network running. The only difference is that the loss part may not have other operations and the mask does not need to be returned. + +For example, we want to select the values of the first 70% positive samples to compute the loss. +The PyTorch implementation is as follows: + +```python +import torch +import torch.nn as torch_nn + +class ClassLoss_pt(torch_nn.Module): + def __init__(self): + super(ClassLoss_pt, self).__init__() + self.con_loss = torch_nn.CrossEntropyLoss(reduction='none') + + def forward(self, pred, label): + mask = label > 0 + vaild_label = label * mask + pos_num = torch.clamp(mask.sum() * 0.7, 1).int() + con = self.con_loss(pred, vaild_label.long()) * mask + loss, _ = torch.topk(con, k=pos_num) + return loss.mean() +``` + +`torch.topk` is used to obtain the first 70% positive sample data. Currently, MindSpore does not support K as a variable. Therefore, you need to convert the method to obtain the Kth largest value and then obtain the mask of the top K based on the value. The MindSpore implementation is as follows: + +```python +import mindspore as ms +from mindspore import ops +from mindspore import nn as ms_nn + +class ClassLoss_ms(ms_nn.Cell): + def __init__(self): + super(ClassLoss_ms, self).__init__() + self.con_loss = ms_nn.SoftmaxCrossEntropyWithLogits(sparse=True, reduction="none") + self.sort_descending = ops.Sort(descending=True) + + def construct(self, pred, label): + mask = label > 0 + vaild_label = label * mask + pos_num = ops.maximum(mask.sum() * 0.7, 1).astype(ms.int32) + con = self.con_loss(pred, vaild_label.astype(ms.int32)) * mask + con_sort, _ = self.sort_descending(con) + con_k = con_sort[pos_num - 1] + con_mask = (con >= con_k).astype(con.dtype) + loss = con * con_mask + return loss.sum() / con_mask.sum() +``` + +Let's look at the test result. + +```python +import torch +import numpy as np +import mindspore as ms +ms.set_seed(1) + +pred = np.random.uniform(0, 1, (5, 2)).astype(np.float32) +label = np.array([-1, 0, 1, 1, 0]).astype(np.int32) +print("pred", pred) +print("label", label) +t_loss = ClassLoss_pt() +cls_loss_pt = t_loss(torch.from_numpy(pred), torch.from_numpy(label)) +print("cls_loss_pt", cls_loss_pt) +m_loss = ClassLoss_ms() +cls_loss_ms = m_loss(ms.Tensor(pred), ms.Tensor(label)) +print("cls_loss_ms", cls_loss_ms) +``` + +```text + pred [[4.17021990e-01 7.20324516e-01] + [1.14374816e-04 3.02332580e-01] + [1.46755889e-01 9.23385918e-02] + [1.86260208e-01 3.45560730e-01] + [3.96767467e-01 5.38816750e-01]] + label [-1 0 1 1 0] + cls_loss_pt tensor(0.7207) + cls_loss_ms 0.7207259 +``` + +#### Shape Changes Introduced by Different Branches of Control Flows + +The following is an example in the model analysis and preparation section: + +```python +import numpy as np +import mindspore as ms +from mindspore import ops +np.random.seed(1) +x = ms.Tensor(np.random.uniform(0, 1, (10)).astype(np.float32)) +cond = (x > 0.5).any() + +if cond: + y = ops.masked_select(x, x > 0.5) +else: + y = ops.zeros_like(x) +print(x) +print(cond) +print(y) +``` + +```text + [4.17021990e-01 7.20324516e-01 1.14374816e-04 3.02332580e-01 + 1.46755889e-01 9.23385918e-02 1.86260208e-01 3.45560730e-01 + 3.96767467e-01 5.38816750e-01] + True + [0.7203245 0.53881675] +``` + +In `cond=True` mode, the maximum shape is the same as x. According to the preceding mask adding method, the maximum shape can be written as follows: + +```python +import numpy as np +import mindspore as ms +from mindspore import ops +np.random.seed(1) +x = ms.Tensor(np.random.uniform(0, 1, (10)).astype(np.float32)) +cond = (x > 0.5).any() + +if cond: + mask = (x > 0.5).astype(x.dtype) +else: + mask = ops.zeros_like(x) +y = x * mask +print(x) +print(cond) +print(y) +``` + +```text + [4.17021990e-01 7.20324516e-01 1.14374816e-04 3.02332580e-01 + 1.46755889e-01 9.23385918e-02 1.86260208e-01 3.45560730e-01 + 3.96767467e-01 5.38816750e-01] + True + [0. 0.7203245 0. 0. 0. 0. + 0. 0. 0. 0.53881675] +``` + +Note that if y is involved in other computations, the mask needs to be transferred together to filter the valid positions. + +## Loss Construction + +The loss function is essentially a part of network construction and can be constructed using `Cell`. For details, see [Loss Function](https://www.mindspore.cn/tutorials/en/master/advanced/modules/loss.html). + +Note that loss generally involves operations such as feature combination, cross entropy, and specification, which are prone to overflow. Therefore, the float16 type is not recommended for loss. A basic network with loss is constructed as follows: + +```python +# 1. Build a network. +net = Net() +# 2. Build a loss. +loss = Loss() +# 3. Perform mixed precision on the network. +net = apply_amp(net) +# 4. Use float32 for the loss part. +loss = loss.to_float(ms.float32) +# 5. Combine the network with loss. +net_with_loss = WithLossCell(net, loss) +net_with_loss.set_train() +# 6. Set a backward flag in PYNATIVE mode. +net_with_loss.set_grad() +``` + +You can also use the [Model](https://www.mindspore.cn/tutorials/en/master/advanced/model/model.html) interface to encapsulate the network and loss in step 5. + +Note that you do not need to set `set_train` and `set_grad` when using `Model` to package the network. The framework sets `set_train` and `set_grad` when executing `model.train`. diff --git a/docs/mindspore/source_en/migration_guide/model_development/model_development.md b/docs/mindspore/source_en/migration_guide/model_development/model_development.md index 35ac314b13..cf186dc953 100644 --- a/docs/mindspore/source_en/migration_guide/model_development/model_development.md +++ b/docs/mindspore/source_en/migration_guide/model_development/model_development.md @@ -6,6 +6,8 @@ This chapter will introduce the related contents of MindSpore scripting, includi ## Network Training Principle +![train_procession.png](images/train_procession.png) + The basic principle of network training is shown in the figure above. The training process of the whole network consists of 5 modules: @@ -22,6 +24,8 @@ The training process of the whole network consists of 5 modules: ## Principles of Network Inference +![evaluation_procession.png](images/evaluation_procession.png) + The basic principles of network inference are shown in the figure above. The training process of the whole network consists of 3 modules: @@ -37,7 +41,7 @@ The training process of the whole network consists of 3 modules: After understanding the process of network training and inference, the following describes how to implement the process of network training and inference on MindSpore. - [Constructing Dataset](https://www.mindspore.cn/docs/en/master/migration_guide/model_development/dataset.html) -- Network Body and Loss Building +- [Network Body and Loss Building](https://www.mindspore.cn/docs/en/master/migration_guide/model_development/model_and_loss.html) - [Learning Rate and Optimizer](https://www.mindspore.cn/docs/en/master/migration_guide/model_development/learning_rate_and_optimizer.html) - [Training Network and Gradient Derivation](https://www.mindspore.cn/docs/en/master/migration_guide/model_development/training_and_gradient.html) - [Inference and Training Process](https://www.mindspore.cn/docs/en/master/migration_guide/model_development/training_and_evaluation_procession.html) @@ -54,7 +58,7 @@ During MindSpore network implementation, there are some problem-prone areas. Whe 1. The MindSpore operator is used in data processing. Multi-threaded/multi-process is usually in the data processing process, so there is a limitation of using MindSpore operators in this scenario. It is recommended to use a three-party implementation instead of the operator use in the data processing process, such as numpy, opencv, pandas, PIL. 2. Control flow. For details, refer to [Flow Control Statements](https://www.mindspore.cn/tutorials/experts/en/master/network/control_flow.html). Compilation in graph mode can be slow when multiple layers of conditional control statements are called. -3. Slicing operation. When it comes to slicing a Tensor, note that whether subscript of the slice is a variable. When it is a variable, there will be restrictions. Please refer to network body and loss building for dynamic shape mitigation. +3. Slicing operation. When it comes to slicing a Tensor, note that whether subscript of the slice is a variable. When it is a variable, there will be restrictions. Please refer to [network body and loss building](https://www.mindspore.cn/docs/en/master/migration_guide/model_development/model_and_loss.html) for dynamic shape mitigation. 4. Customized mixed precision conflicts with `amp_level` in Model, so don't set `amp_level` in Model if you use customized mixed precision. 5. In Ascend environment, Conv, Sort and TopK can only be float16, and add [loss scale](https://mindspore.cn/tutorials/experts/en/master/others/mixed_precision.html) to avoid overflow. 6. In the Ascend environment, operators with the stride property such as Conv and Pooling have rules about the length of the stride, which needs to be mitigated. diff --git a/docs/mindspore/source_en/migration_guide/model_development/training_and_evaluation_procession.md b/docs/mindspore/source_en/migration_guide/model_development/training_and_evaluation_procession.md index c16528b70e..c335265aed 100644 --- a/docs/mindspore/source_en/migration_guide/model_development/training_and_evaluation_procession.md +++ b/docs/mindspore/source_en/migration_guide/model_development/training_and_evaluation_procession.md @@ -114,7 +114,7 @@ The inference process cannot be encapsulated into a Model for operation sometime In the model analysis and preparation phase, we get the trained parameters of the reference implementation (in the reference implementation README or for training replication). Since the implementation of the model algorithm is not related to the framework, the trained parameters can be first converted into MindSpore [checkpoint](https://www.mindspore.cn/tutorials/en/master/beginner/save_load.html) and loaded into the network for inference verification. -Please refer to resnet network migration for the whole process of inference verification. +Please refer to [resnet network migration](https://www.mindspore.cn/docs/en/master/migration_guide/sample_code.html) for the whole process of inference verification. ## Training Process diff --git a/docs/mindspore/source_en/migration_guide/overview.md b/docs/mindspore/source_en/migration_guide/overview.md index d2b7b394a6..7e67dc83a7 100644 --- a/docs/mindspore/source_en/migration_guide/overview.md +++ b/docs/mindspore/source_en/migration_guide/overview.md @@ -19,7 +19,7 @@ In this process, we have a relatively complete description of each link. We hope Network migration starts with configuring the MindSpore development environment, and this chapter describes the installation process and knowledge preparation in detail. The knowledge preparation includes a basic introduction to the MindSpore components models and hub, including the purpose, scenarios and usage. There are also tutorials on training on the cloud: using ModelArts to adapt scripts, uploading datasets in OBS, and training online. -## Model analysis and preparation +## [Model analysis and preparation](https://www.mindspore.cn/docs/en/master/migration_guide/analysis_and_preparation.html) Before doing formal development, some analysis preparation work needs to be done on the network/algorithm to be migrated, including: @@ -27,20 +27,20 @@ Before doing formal development, some analysis preparation work needs to be done - Reproducing the results of the paper, obtaining the base model (ckpt), benchmark accuracy and performance - Analyzing the APIs and functions used in the network. -When migrating networks from PyTorch to MindSpore, users need to be aware of differences from typical PyTorch interfaces. +When migrating networks from PyTorch to MindSpore, users need to be aware of [differences from typical PyTorch interfaces](https://www.mindspore.cn/docs/en/master/migration_guide/typical_api_comparision.html). ## [MindSpore model implementation](https://www.mindspore.cn/docs/en/master/migration_guide/model_development/model_development.html) After the preliminary analysis preparation, you can develop the new network by using MindSpore. This chapter will introduce the knowledge of MindSpore network construction and the process of training and inference, starting from the basic modules during inference and training, and using one or two examples to illustrate how to build the network in special scenarios. -## Debugging and Tuning +## [Debugging and Tuning](https://www.mindspore.cn/docs/en/master/migration_guide/debug_and_tune.html) This chapter will introduce some methods of debugging and tuning from three aspects: function, precision and performance. -## Example of Network Migration Debugging +## [Example of Network Migration Debugging](https://www.mindspore.cn/docs/en/master/migration_guide/sample_code.html) This chapter contains a complete network migration sample. From the analysis and replication of the benchmark network, it details the steps of script development and precision debugging and tuning, and finally lists the common problems and corresponding optimization methods during the migration process, framework performance issues. -## FAQs +## [FAQs](https://www.mindspore.cn/docs/en/master/migration_guide/faq.html) This chapter lists the frequently-asked questions and corresponding solutions. diff --git a/docs/mindspore/source_en/migration_guide/sample_code.md b/docs/mindspore/source_en/migration_guide/sample_code.md new file mode 100644 index 0000000000..6eb5647482 --- /dev/null +++ b/docs/mindspore/source_en/migration_guide/sample_code.md @@ -0,0 +1,840 @@ +# Network Migration Debugging Example + + + +The following uses the classic network ResNet-50 as an example to describe the network migration method in detail based on the code. + +## Model Analysis and Preparation + +Assume that the MindSpore operating environment has been configured according to [Environment Preparation and Information Acquisition](https://www.mindspore.cn/docs/en/master/migration_guide/enveriment_preparation.html). Assume that ResNet-50 has not been implemented in the models repository. + +First, analyze the algorithm and network structure. + +The Residual Neural Network (ResNet) was proposed by Kaiming He et al. from Microsoft Research Institute. They used residual units to successfully train a 152-layer neural network, and thus became the winner of ILSVRC 2015. A conventional convolutional network or fully-connected network has more or less information losses, and further causes gradient disappearance or explosion. As a result, deep network training fails. The ResNet can solve these problems to some extent. By passing the input information to the output, the information integrity is protected. The network only needs to learn the differences between the input and output, simplifying the learning objective and difficulty. Its structure can accelerate training of a neural network and greatly improve the accuracy of the network model. + +[Paper](https://arxiv.org/pdf/1512.03385.pdf): Kaiming He, Xiangyu Zhang, Shaoqing Ren, Jian Sun."Deep Residual Learning for Image Recognition" + +The [sample code of PyTorch ResNet-50 CIFAR-10](https://gitee.com/mindspore/docs/tree/master/docs/mindspore/source_zh_cn/migration_guide/code/resnet_convert/resnet_pytorch) contains the PyTorch ResNet implementation, CIFAR-10 data processing, network training, and inference processes. + +### Checklist + +When reading the paper and referring to the implementation, analyze and fill in the following checklist: + +|Trick|Record| +|----|----| +|Data augmentation| RandomCrop, RandomHorizontalFlip, Resize, Normalize| +|Learning rate attenuation policy| Fixed learning rate = 0.001| +|Optimization parameters| Adam optimizer, weight_decay = 1e-5| +|Training parameters| batch_size = 32, epochs = 90| +|Network structure optimization| Bottleneck | +|Training process optimization| None| + +### Reproducing Reference Implementation + +Download the PyTorch code and CIFAR-10 dataset to train the network. + +```text +Train Epoch: 89 [0/1563 (0%)] Loss: 0.010917 +Train Epoch: 89 [100/1563 (6%)] Loss: 0.013386 +Train Epoch: 89 [200/1563 (13%)] Loss: 0.078772 +Train Epoch: 89 [300/1563 (19%)] Loss: 0.031228 +Train Epoch: 89 [400/1563 (26%)] Loss: 0.073462 +Train Epoch: 89 [500/1563 (32%)] Loss: 0.098645 +Train Epoch: 89 [600/1563 (38%)] Loss: 0.112967 +Train Epoch: 89 [700/1563 (45%)] Loss: 0.137923 +Train Epoch: 89 [800/1563 (51%)] Loss: 0.143274 +Train Epoch: 89 [900/1563 (58%)] Loss: 0.088426 +Train Epoch: 89 [1000/1563 (64%)] Loss: 0.071185 +Train Epoch: 89 [1100/1563 (70%)] Loss: 0.094342 +Train Epoch: 89 [1200/1563 (77%)] Loss: 0.126669 +Train Epoch: 89 [1300/1563 (83%)] Loss: 0.245604 +Train Epoch: 89 [1400/1563 (90%)] Loss: 0.050761 +Train Epoch: 89 [1500/1563 (96%)] Loss: 0.080932 + +Test set: Average loss: -9.7052, Accuracy: 91% + +Finished Training +``` + +You can download training logs and saved parameter files from [resnet_pytorch_res](https://mindspore-website.obs.cn-north-4.myhuaweicloud.com/notebook/models/resnet_pytorch_res.zip). + +### Analyzing API/Feature Missing + +- API analysis + +| PyTorch API | MindSpore API| Different or Not| +| ---------------------- | ------------------ | ------| +| `nn.Conv2D` | `nn.Conv2d` | Yes. [Difference](https://www.mindspore.cn/docs/en/master/note/api_mapping/pytorch_diff/nn_Conv2d.html)| +| `nn.BatchNorm2D` | `nn.BatchNom2d` | Yes. [Difference](https://www.mindspore.cn/docs/en/master/note/api_mapping/pytorch_diff/BatchNorm2d.html)| +| `nn.ReLU` | `nn.ReLU` | No| +| `nn.MaxPool2D` | `nn.MaxPool2d` | Yes. [Difference](https://www.mindspore.cn/docs/en/master/note/api_mapping/pytorch_diff/MaxPool2d.html)| +| `nn.AdaptiveAvgPool2D` | `nn.AdaptiveAvgPool2D` | No | +| `nn.Linear` | `nn.Dense` | Yes. [Difference](https://www.mindspore.cn/docs/en/master/note/api_mapping/pytorch_diff/Dense.html)| +| `torch.flatten` | `nn.Flatten` | No| + +By checking [PyTorch API Mapping] (https://www.mindspore.cn/docs/en/master/note/api_mapping/pytorch_api_mapping.html), we find that four APIs are different. + +- Function analysis + +| PyTorch Function | MindSpore Function | +| ------------------------- | ------------------------------------- | +| `nn.init.kaiming_normal_` | `initializer(init='HeNormal')` | +| `nn.init.constant_` | `initializer(init='Constant')` | +| `nn.Sequential` | `nn.SequentialCell` | +| `nn.Module` | `nn.Cell` | +| `nn.distibuted` | `set_auto_parallel_context` | +| `torch.optim.SGD` | `nn.optim.SGD` or `nn.optim.Momentum` | + +(The interface design of MindSpore is different from that of PyTorch. Therefore, only the comparison of key functions is listed here.) + +After API and function analysis, we find that there are no missing APIs and functions on MindSpore compared with PyTorch. + +## MindSpore Model Implementation + +### Datasets + +The CIFAR-10 dataset of PyTorch is processed as follows: + +```python +import torch +import torchvision.transforms as trans +import torchvision + +train_transform = trans.Compose([ + trans.RandomCrop(32, padding=4), + trans.RandomHorizontalFlip(0.5), + trans.Resize(224), + trans.ToTensor(), + trans.Normalize([0.4914, 0.4822, 0.4465], [0.2023, 0.1994, 0.2010]), +]) +test_transform = trans.Compose([ + trans.Resize(224), + trans.RandomHorizontalFlip(0.5), + trans.ToTensor(), + trans.Normalize([0.4914, 0.4822, 0.4465], [0.2023, 0.1994, 0.2010]), +]) +train_set = torchvision.datasets.CIFAR10(root='./data', train=True, transform=train_transform) +train_loader = torch.utils.data.DataLoader(train_set, batch_size=32, shuffle=True) +test_set = torchvision.datasets.CIFAR10(root='./data', train=False, transform=test_transform) +test_loader = torch.utils.data.DataLoader(test_set, batch_size=1, shuffle=False) +``` + +If the CIFAR-10 dataset does not exist on the local host, you can add `download=True` when using `torchvision.datasets.CIFAR10` to automatically download the dataset. + +The CIFAR-10 dataset directory is organized as follows: + +```text +└─dataset_path + ├─cifar-10-batches-bin # train dataset + ├─ data_batch_1.bin + ├─ data_batch_2.bin + ├─ data_batch_3.bin + ├─ data_batch_4.bin + ├─ data_batch_5.bin + └─cifar-10-verify-bin # evaluate dataset + ├─ test_batch.bin +``` + +This operation is implemented on MindSpore as follows: + +```python +import mindspore as ms +import mindspore.dataset as ds +from mindspore.dataset import vision +from mindspore.dataset.transforms.transforms import TypeCast + +def create_cifar_dataset(dataset_path, do_train, batch_size=32, image_size=(224, 224), rank_size=1, rank_id=0): + dataset = ds.Cifar10Dataset(dataset_path, shuffle=do_train, + num_shards=rank_size, shard_id=rank_id) + + # define map operations + trans = [] + if do_train: + trans += [ + vision.RandomCrop((32, 32), (4, 4, 4, 4)), + vision.RandomHorizontalFlip(prob=0.5) + ] + + trans += [ + vision.Resize(image_size), + vision.Rescale(1.0 / 255.0, 0.0), + vision.Normalize([0.4914, 0.4822, 0.4465], [0.2023, 0.1994, 0.2010]), + vision.HWC2CHW() + ] + + type_cast_op = TypeCast(ms.int32) + + data_set = dataset.map(operations=type_cast_op, input_columns="label") + data_set = data_set.map(operations=trans, input_columns="image") + + # apply batch operations + data_set = data_set.batch(batch_size, drop_remainder=do_train) + return data_set +``` + +### Network Model Implementation + +By referring to [PyTorch ResNet](https://gitee.com/mindspore/docs/blob/master/docs/mindspore/source_zh_cn/migration_guide/code/resnet_convert/resnet_pytorch/resnet.py), we have implemented [MindSpore ResNet](https://gitee.com/mindspore/docs/blob/master/docs/mindspore/source_zh_cn/migration_guide/code/resnet_convert/resnet_ms/src/resnet.py). The comparison tool shows that the implementation is different in the following aspects: + +```python +# Conv2d PyTorch +nn.Conv2d( + in_planes, + out_planes, + kernel_size=3, + stride=stride, + padding=dilation, + groups=groups, + bias=False, + dilation=dilation, +) +########################################## + +# Conv2d MindSpore +nn.Conv2d( + in_planes, + out_planes, + kernel_size=3, + pad_mode="pad", + stride=stride, + padding=dilation, + group=groups, + has_bias=False, + dilation=dilation, +) +``` + +```python +# PyTorch +nn.Module +############################################ +# MindSpore +nn.Cell +``` + +```python +# PyTorch +nn.ReLU(inplace=True) +############################################ +# MindSpore +nn.ReLU() +``` + +```python +# PyTorch graph construction +forward +############################################ +# MindSpore graph construction +construct +``` + +```python +# PyTorch MaxPool2d with padding +maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) +############################################ +# MindSpore MaxPool2d with padding +maxpool = nn.SequentialCell([ + nn.Pad(paddings=((0, 0), (0, 0), (1, 1), (1, 1)), mode="CONSTANT"), + nn.MaxPool2d(kernel_size=3, stride=2)]) +``` + +```python +# PyTorch AdaptiveAvgPool2d +avgpool = nn.AdaptiveAvgPool2d((1, 1)) +############################################ +# When PyTorch AdaptiveAvgPool2d output shape is set to 1, MindSpore ReduceMean functions the same with higher speed. +mean = ops.ReduceMean(keep_dims=True) +``` + +```python +# PyTorch full connection +fc = nn.Linear(512 * block.expansion, num_classes) +############################################ +# MindSpore full connection +fc = nn.Dense(512 * block.expansion, num_classes) +``` + +```python +# PyTorch Sequential +nn.Sequential +############################################ +# MindSpore SequentialCell +nn.SequentialCell +``` + +```python +# PyTorch initialization +for m in self.modules(): + if isinstance(m, nn.Conv2d): + nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu") + elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)): + nn.init.constant_(m.weight, 1) + nn.init.constant_(m.bias, 0) + +# Zero-initialize the last BN in each residual branch, +# so that the residual branch starts with zeros, and each residual block behaves like an identity. +# This improves the model by 0.2~0.3% according to https://arxiv.org/abs/1706.02677 +if zero_init_residual: + for m in self.modules(): + if isinstance(m, Bottleneck) and m.bn3.weight is not None: + nn.init.constant_(m.bn3.weight, 0) # type: ignore[arg-type] + elif isinstance(m, BasicBlock) and m.bn2.weight is not None: + nn.init.constant_(m.bn2.weight, 0) # type: ignore[arg-type] + +############################################ + +# MindSpore initialization +for _, cell in self.cells_and_names(): + if isinstance(cell, nn.Conv2d): + cell.weight.set_data(ms.common.initializer.initializer( + ms.common.initializer.HeNormal(negative_slope=0, mode='fan_out', nonlinearity='relu'), + cell.weight.shape, cell.weight.dtype)) + elif isinstance(cell, (nn.BatchNorm2d, nn.GroupNorm)): + cell.gamma.set_data(ms.common.initializer.initializer("ones", cell.gamma.shape, cell.gamma.dtype)) + cell.beta.set_data(ms.common.initializer.initializer("zeros", cell.beta.shape, cell.beta.dtype)) + elif isinstance(cell, (nn.Dense)): + cell.weight.set_data(ms.common.initializer.initializer( + ms.common.initializer.HeUniform(negative_slope=math.sqrt(5)), + cell.weight.shape, cell.weight.dtype)) + cell.bias.set_data(ms.common.initializer.initializer("zeros", cell.bias.shape, cell.bias.dtype)) + +# Zero-initialize the last BN in each residual branch, +# so that the residual branch starts with zeros, and each residual block behaves like an identity. +# This improves the model by 0.2~0.3% according to https://arxiv.org/abs/1706.02677 +if zero_init_residual: + for _, cell in self.cells_and_names(): + if isinstance(cell, Bottleneck) and cell.bn3.gamma is not None: + cell.bn3.gamma.set_data("zeros", cell.bn3.gamma.shape, cell.bn3.gamma.dtype) + elif isinstance(cell, BasicBlock) and cell.bn2.weight is not None: + cell.bn2.gamma.set_data("zeros", cell.bn2.gamma.shape, cell.bn2.gamma.dtype) +``` + +### Loss Function + +PyTorch: + +```python +net_loss = torch.nn.CrossEntropyLoss() +``` + +MindSpore: + +```python +loss = nn.SoftmaxCrossEntropyWithLogits(sparse=True, reduction='mean') +``` + +### Learning Rate and Optimizer + +PyTorch: + +```python +net_opt = torch.optim.Adam(net.parameters(), 0.001, weight_decay=1e-5) +``` + +MindSpore: + +```python +optimizer = nn.Adam(resnet.trainable_params(), 0.001, weight_decay=1e-5) +``` + +## Model Validation + +The trained PyTorch parameters are obtained in [Reproducing Reference Implementation](#reproducing-reference-implementation). How do I convert the parameter file into a checkpoint file that can be used by MindSpore? + +The following steps are required: + +1. Print the names and shapes of all parameters in the PyTorch parameter file and the names and shapes of all parameters in the MindSpore cell to which parameters need to be loaded. +2. Compare the parameter name and shape to construct the parameter mapping. +3. Create a parameter list based on the parameter mapping (PyTorch parameters -> numpy -> MindSpore parameters) and save the parameter list as a checkpoint. +4. Unit test: Load PyTorch parameters and MindSpore parameters, construct random input, and compare the output. + +### Printing Parameters + +```python +import torch +# Print the parameter names and shapes of all parameters in the PyTorch parameter file and return the parameter dictionary. +def pytorch_params(pth_file): + par_dict = torch.load(pth_file, map_location='cpu') + pt_params = {} + for name in par_dict: + parameter = par_dict[name] + print(name, parameter.numpy().shape) + pt_params[name] = parameter.numpy() + return pt_params + +# Print the names and shapes of all parameters in the MindSpore cell and return the parameter dictionary. +def mindspore_params(network): + ms_params = {} + for param in network.get_parameters(): + name = param.name + value = param.data.asnumpy() + print(name, value.shape) + ms_params[name] = value + return ms_params +``` + +Run the following code: + +```python +from resnet_ms.src.resnet import resnet50 as ms_resnet50 +pth_path = "resnet.pth" +pt_param = pytorch_params(pth_path) +print("="*20) +ms_param = mindspore_params(ms_resnet50(num_classes=10)) +``` + +You can obtain the following result: + +```text +conv1.weight (64, 3, 7, 7) +bn1.weight (64,) +bn1.bias (64,) +bn1.running_mean (64,) +bn1.running_var (64,) +bn1.num_batches_tracked () +layer1.0.conv1.weight (64, 64, 1, 1) +...... +=========================================== +conv1.weight (64, 3, 7, 7) +bn1.moving_mean (64,) +bn1.moving_variance (64,) +bn1.gamma (64,) +bn1.beta (64,) +layer1.0.conv1.weight (64, 64, 1, 1) +...... +``` + +### Parameter Mapping and Checkpoint Saving + +Except the BatchNorm parameter, the names and shapes of other parameters are correct. In this case, you can write a simple Python script for parameter mapping. + +```python +import mindspore as ms +def param_convert(ms_params, pt_params, ckpt_path): + # Parameter name mapping dictionary + bn_ms2pt = {"gamma": "weight", + "beta": "bias", + "moving_mean": "running_mean", + "moving_variance": "running_var"} + new_params_list = [] + for ms_param in ms_params.keys(): + # In the parameter list, only the parameters that contain bn and downsample.1 are the parameters of the BatchNorm operator. + if "bn" in ms_param or "downsample.1" in ms_param: + ms_param_item = ms_param.split(".") + pt_param_item = ms_param_item[:-1] + [bn_ms2pt[ms_param_item[-1]]] + pt_param = ".".join(pt_param_item) + # If the corresponding parameter is found and the shape is the same, add the parameter to the parameter list. + if pt_param in pt_params and pt_params[pt_param].shape == ms_params[ms_param].shape: + ms_value = pt_params[pt_param] + new_params_list.append({"name": ms_param, "data": ms.Tensor(ms_value)}) + else: + print(ms_param, "not match in pt_params") + # Other parameters + else: + # If the corresponding parameter is found and the shape is the same, add the parameter to the parameter list. + if ms_param in pt_params and pt_params[ms_param].shape == ms_params[ms_param].shape: + ms_value = pt_params[ms_param] + new_params_list.append({"name": ms_param, "data": ms.Tensor(ms_value)}) + else: + print(ms_param, "not match in pt_params") + # Save as MindSpore checkpoint. + ms.save_checkpoint(new_params_list, ckpt_path) + +ckpt_path = "resnet50.ckpt" +param_convert(ms_params, pt_params, ckpt_path) +``` + +After the execution is complete, you can find the generated checkpoint file in `ckpt_path`. + +If the parameter mapping is complex and it is difficult to find the mapping based on the parameter name, you can write a parameter mapping dictionary, for example: + +```python +param = { + 'bn1.bias': 'bn1.beta', + 'bn1.weight': 'bn1.gamma', + 'IN.weight': 'IN.gamma', + 'IN.bias': 'IN.beta', + 'BN.bias': 'BN.beta', + 'in.weight': 'in.gamma', + 'bn.weight': 'bn.gamma', + 'bn.bias': 'bn.beta', + 'bn2.weight': 'bn2.gamma', + 'bn2.bias': 'bn2.beta', + 'bn3.bias': 'bn3.beta', + 'bn3.weight': 'bn3.gamma', + 'BN.running_mean': 'BN.moving_mean', + 'BN.running_var': 'BN.moving_variance', + 'bn.running_mean': 'bn.moving_mean', + 'bn.running_var': 'bn.moving_variance', + 'bn1.running_mean': 'bn1.moving_mean', + 'bn1.running_var': 'bn1.moving_variance', + 'bn2.running_mean': 'bn2.moving_mean', + 'bn2.running_var': 'bn2.moving_variance', + 'bn3.running_mean': 'bn3.moving_mean', + 'bn3.running_var': 'bn3.moving_variance', + 'downsample.1.running_mean': 'downsample.1.moving_mean', + 'downsample.1.running_var': 'downsample.1.moving_variance', + 'downsample.0.weight': 'downsample.1.weight', + 'downsample.1.bias': 'downsample.1.beta', + 'downsample.1.weight': 'downsample.1.gamma' +} +``` + +Then, you can obtain the parameter file based on the `param_convert` process. + +### Unit Test + +After obtaining the corresponding parameter file, you need to perform a unit test on the entire model to ensure model consistency. + +```python +import numpy as np +import torch +import mindspore as ms +from resnet_ms.src.resnet import resnet50 as ms_resnet50 +from resnet_pytorch.resnet import resnet50 as pt_resnet50 + +def check_res(pth_path, ckpt_path): + inp = np.random.uniform(-1, 1, (4, 3, 224, 224)).astype(np.float32) + # When performing a unit test, you need to add a training or inference label to the cell. + ms_resnet = ms_resnet50(num_classes=10).set_train(False) + pt_resnet = pt_resnet50(num_classes=10).eval() + pt_resnet.load_state_dict(torch.load(pth_path, map_location='cpu')) + ms.load_checkpoint(ckpt_path, ms_resnet) + print("========= pt_resnet conv1.weight ==========") + print(pt_resnet.conv1.weight.detach().numpy().reshape((-1,))[:10]) + print("========= ms_resnet conv1.weight ==========") + print(ms_resnet.conv1.weight.data.asnumpy().reshape((-1,))[:10]) + pt_res = pt_resnet(torch.from_numpy(inp)) + ms_res = ms_resnet(ms.Tensor(inp)) + print("========= pt_resnet res ==========") + print(pt_res) + print("========= ms_resnet res ==========") + print(ms_res) + print("diff", np.max(np.abs(pt_res.detach().numpy() - ms_res.asnumpy()))) + +pth_path = "resnet.pth" +ckpt_path = "resnet50.ckpt" +check_res(pth_path, ckpt_path) +``` + +During the unit test, you need to add training or inference labels to cells. PyTorch training uses `.train()` and inference uses `.eval()`, MindSpore training uses `.set_train()` and inference uses `.set_train(False)`. + +```text +========= pt_resnet conv1.weight ========== +[ 1.091892e-40 -1.819391e-39 3.509566e-40 -8.281730e-40 1.207908e-39 + -3.576954e-41 -1.000796e-39 1.115791e-39 -1.077758e-39 -6.031427e-40] +========= ms_resnet conv1.weight ========== +[ 1.091892e-40 -1.819391e-39 3.509566e-40 -8.281730e-40 1.207908e-39 + -3.576954e-41 -1.000796e-39 1.115791e-39 -1.077758e-39 -6.031427e-40] +========= pt_resnet res ========== +tensor([[-15.1945, -5.6529, 6.5738, 9.7807, -2.4615, 3.0365, -4.7216, + -11.1005, 2.7121, -9.3612], + [-14.2412, -5.9004, 5.6366, 9.7030, -1.6322, 2.6926, -3.7307, + -10.7582, 1.4195, -7.9930], + [-13.4795, -5.6582, 5.6432, 8.9152, -1.5169, 2.6958, -3.4469, + -10.5300, 1.3318, -8.1476], + [-13.6448, -5.4239, 5.8254, 9.3094, -2.1969, 2.7042, -4.1194, + -10.4388, 1.9331, -8.1746]], grad_fn=) +========= ms_resnet res ========== +[[-15.194535 -5.652934 6.5737996 9.780719 -2.4615316 3.0365033 + -4.7215843 -11.100524 2.7121294 -9.361177 ] + [-14.24116 -5.9004383 5.6366115 9.702984 -1.6322318 2.69261 + -3.7307222 -10.758192 1.4194587 -7.992969 ] + [-13.47945 -5.658216 5.6432185 8.915173 -1.5169426 2.6957715 + -3.446888 -10.529953 1.3317728 -8.147601 ] + [-13.644804 -5.423854 5.825424 9.309403 -2.1969485 2.7042081 + -4.119426 -10.438771 1.9330862 -8.174606 ]] +diff 2.861023e-06 +``` + +The final result is similar and basically meets the expectation. If the result difference is large, you need to compare the result layer by layer. + +## Inference Process + +PyTorch inference: + +```python +import torch +import torchvision.transforms as trans +import torchvision +import torch.nn.functional as F +from resnet import resnet50 + +def test_epoch(model, device, data_loader): + model.eval() + test_loss = 0 + correct = 0 + with torch.no_grad(): + for data, target in data_loader: + output = model(data.to(device)) + test_loss += F.nll_loss(output, target.to(device), reduction='sum').item() # sum up batch loss + pred = output.max(1)[1] # get the index of the max log-probability + correct += pred.eq(target.to(device)).sum().item() + + test_loss /= len(data_loader.dataset) + print('\nTest set: Average loss: {:.4f}, Accuracy: {:.0f}%\n'.format( + test_loss, 100. * correct / len(data_loader.dataset))) + +use_cuda = torch.cuda.is_available() +device = torch.device("cuda" if use_cuda else "cpu") +test_transform = trans.Compose([ + trans.Resize(224), + trans.RandomHorizontalFlip(0.5), + trans.ToTensor(), + trans.Normalize([0.4914, 0.4822, 0.4465], [0.2023, 0.1994, 0.2010]), +]) +test_set = torchvision.datasets.CIFAR10(root='./data', train=False, transform=test_transform) +test_loader = torch.utils.data.DataLoader(test_set, batch_size=1, shuffle=False) +# 2. define forward network +net = resnet50(num_classes=10).cuda() if use_cuda else resnet50(num_classes=10) +net.load_state_dict(torch.load("./resnet.pth", map_location='cpu')) +test_epoch(net, device, test_loader) +``` + +```text +Test set: Average loss: -9.7075, Accuracy: 91% +``` + +MindSpore implements this process: + +```python +import numpy as np +import mindspore as ms +from mindspore import nn +from src.dataset import create_dataset +from src.model_utils.moxing_adapter import moxing_wrapper +from src.model_utils.config import config +from src.utils import init_env +from src.resnet import resnet50 + + +def test_epoch(model, data_loader, loss_func): + model.set_train(False) + test_loss = 0 + correct = 0 + for data, target in data_loader: + output = model(data) + test_loss += float(loss_func(output, target).asnumpy()) + pred = np.argmax(output.asnumpy(), axis=1) + correct += (pred == target.asnumpy()).sum() + dataset_size = data_loader.get_dataset_size() + test_loss /= dataset_size + print('\nTest set: Average loss: {:.4f}, Accuracy: {:.0f}%\n'.format( + test_loss, 100. * correct / dataset_size)) + + +@moxing_wrapper() +def test_net(): + init_env(config) + eval_dataset = create_dataset(config.dataset_name, config.data_path, False, batch_size=1, + image_size=(int(config.image_height), int(config.image_width))) + resnet = resnet50(num_classes=config.class_num) + ms.load_checkpoint(config.checkpoint_path, resnet) + loss = nn.SoftmaxCrossEntropyWithLogits(sparse=True, reduction='mean') + test_epoch(resnet, eval_dataset, loss) + + +if __name__ == '__main__': + test_net() +``` + +Run the following command: + +```shell +python test.py --data_path data/cifar10/ --checkpoint_path resnet.ckpt +``` + +You can obtain the following inference accuracy result: + +```text +run standalone! +Test set: Average loss: 0.3240, Accuracy: 91% +``` + +The inference accuracy is the same. + +## Training Process + +For details about the PyTorch training process, see [PyToch ResNet-50 CIFAR-10 Sample Code](https://gitee.com/mindspore/docs/tree/master/docs/mindspore/source_zh_cn/migration_guide/code/resnet_convert/resnet_pytorch). The log file and trained path are stored in [resnet_pytorch_res](https://mindspore-website.obs.cn-north-4.myhuaweicloud.com/notebook/models/resnet_pytorch_res.zip). + +The corresponding MindSpore code is as follows: + +```python +import numpy as np +import mindspore as ms +from mindspore import nn +from mindspore.profiler import Profiler +from src.dataset import create_dataset +from src.model_utils.moxing_adapter import moxing_wrapper +from src.model_utils.config import config +from src.utils import init_env +from src.resnet import resnet50 + + +def train_epoch(epoch, model, data_loader): + model.set_train() + dataset_size = data_loader.get_dataset_size() + for batch_idx, (data, target) in enumerate(data_loader): + loss = float(model(data, target)[0].asnumpy()) + if batch_idx % 100 == 0: + print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format( + epoch, batch_idx, dataset_size, + 100. * batch_idx / dataset_size, loss)) + + +def test_epoch(model, data_loader, loss_func): + model.set_train(False) + test_loss = 0 + correct = 0 + for data, target in data_loader: + output = model(data) + test_loss += float(loss_func(output, target).asnumpy()) + pred = np.argmax(output.asnumpy(), axis=1) + correct += (pred == target.asnumpy()).sum() + dataset_size = data_loader.get_dataset_size() + test_loss /= dataset_size + print('\nTest set: Average loss: {:.4f}, Accuracy: {:.0f}%\n'.format( + test_loss, 100. * correct / dataset_size)) + + +@moxing_wrapper() +def train_net(): + init_env(config) + if config.enable_profiling: + profiler = Profiler() + train_dataset = create_dataset(config.dataset_name, config.data_path, True, batch_size=config.batch_size, + image_size=(int(config.image_height), int(config.image_width)), + rank_size=40, rank_id=config.rank_id) + eval_dataset = create_dataset(config.dataset_name, config.data_path, False, batch_size=1, + image_size=(int(config.image_height), int(config.image_width))) + config.steps_per_epoch = train_dataset.get_dataset_size() + resnet = resnet50(num_classes=config.class_num) + optimizer = nn.Adam(resnet.trainable_params(), config.lr, weight_decay=config.weight_decay) + loss = nn.SoftmaxCrossEntropyWithLogits(sparse=True, reduction='mean') + train_net = nn.TrainOneStepWithLossScaleCell( + nn.WithLossCell(resnet, loss), optimizer, ms.Tensor(config.loss_scale, ms.float32)) + for epoch in range(config.epoch_size): + train_epoch(epoch, train_net, train_dataset) + test_epoch(resnet, eval_dataset, loss) + + print('Finished Training') + save_path = './resnet.ckpt' + ms.save_checkpoint(resnet, save_path) + + +if __name__ == '__main__': + train_net() +``` + +## Performance Optimization + +During the preceding training, it is found that the training is slow and performance optimization is required. Before performing specific optimization items, run the profiler tool to obtain the performance data. The profiler tool can obtain only the training encapsulated by the model. Therefore, you need to reconstruct the training process first. + +```python +device_num = config.device_num +if config.use_profilor: + profiler = Profiler() + # Note that the profiling data should not be too large. Otherwise, the processing will be slow. In this example, if use_profilor is set to True, the original dataset is divided into 40 copies. + device_num = 40 +train_dataset = create_dataset(config.dataset_name, config.data_path, True, batch_size=config.batch_size, + image_size=(int(config.image_height), int(config.image_width)), + rank_size=device_num, rank_id=config.rank_id) +..... +loss_scale = ms.FixedLossScaleManager(config.loss_scale, drop_overflow_update=False) +model = ms.Model(resnet, loss_fn=loss, optimizer=optimizer, loss_scale_manager=loss_scale) +if config.use_profilor: + # Note that the profiling data should not be too large. Otherwise, the processing will be slow. + model.train(3, train_dataset, callbacks=[LossMonitor(), TimeMonitor()], dataset_sink_mode=True) + profiler.analyse() +else: + model.train(config.epoch_size, train_dataset, eval_dataset, callbacks=[LossMonitor(), TimeMonitor()], + dataset_sink_mode=False) +``` + +Set `use_profilor=True`. The `data` directory is generated in the running directory. Rename the directory `profiler_v1` and run the `mindinsight start` command in the same directory. + +![resnet_profiler1](https://gitee.com/mindspore/docs/blob/master/docs/mindspore/source_zh_cn/migration_guide/images/resnet_profiler1.png) + +The following figure shows the MindInsight profiler page. (This analysis is performed in the Ascend environment, which is similar to that in the GPU. The CPU does not support profiler.) There are three parts on the page. + +The first part is step trace, which is the most basic part for profiler. The data of a single device includes the step interval and forward and backward propagation. The forward and backward time is the actual running time of the model on the device, and the step interval time includes data processing, data printing, and time when parameters are saved on the CPU during the training process. +It can be seen that the step trace time and forward and backward execution time are almost even, and non-device operations such as data processing account for a large part. + +The second part is the forward and backward network execution time, where you can view details. + +![resnet_profiler2](https://gitee.com/mindspore/docs/blob/master/docs/mindspore/source_zh_cn/migration_guide/images/resnet_profiler2.png) + +The upper part shows the proportion of each AI CORE operator to the total time, and the lower part shows the details of each operator. + +![resnet_profiler3](https://gitee.com/mindspore/docs/blob/master/docs/mindspore/source_zh_cn/migration_guide/images/resnet_profiler3.png) + +You can click an operator to obtain the execution time, scope, shape, and type of the operator. + +In addition to the AI CORE operators, there may be AI CPU and HOST CPU operators on the network. These operators take more time than the AI CORE operators. You can click the tabs to view the time. + +![resnet_profiler4](https://gitee.com/mindspore/docs/blob/master/docs/mindspore/source_zh_cn/migration_guide/images/resnet_profiler4.png) + +In addition to viewing the operator performance, you can also view the raw data for analysis. + +![resnet_profiler5](https://gitee.com/mindspore/docs/blob/master/docs/mindspore/source_zh_cn/migration_guide/images/resnet_profiler5.png) + +Go to the `profiler_v1/profiler/` directory and click the `aicore_intermediate_0_type.csv` file to view the statistics of each operator. There are 30 AI Core operators in total. The total execution time is 37.526 ms. + +![resnet_profiler6](https://gitee.com/mindspore/docs/blob/master/docs/mindspore/source_zh_cn/migration_guide/images/resnet_profiler6.png) + +In addition, `aicore_intermediate_0_detail.csv` contains detailed data of each operator, which is similar to the operator details displayed in MindInsight. `ascend_timeline_display_0.json` is a timeline data file. For details, see [timeline](https://www.mindspore.cn/mindinsight/docs/en/master/performance_profiling_ascend.html#timeline-analysis). + +The third part is the performance data during data processing. You can view the data queue status in this part. + +![resnet_profiler7](https://gitee.com/mindspore/docs/blob/master/docs/mindspore/source_zh_cn/migration_guide/images/resnet_profiler7.png) + +And a queue status of each data processing operation: + +![resnet_profiler8](https://gitee.com/mindspore/docs/blob/master/docs/mindspore/source_zh_cn/migration_guide/images/resnet_profiler8.png) + +Now, let's analyze the process and solve the problem. + +From the step trace, the step interval and forward and backward execution time are almost even. MindSpore provides an [on-device execution method](https://www.mindspore.cn/docs/en/master/design/overview.html) to concurrently process data and execute the network on the device. You only need to set `dataset_sink_mode=True` in `model.train`. Note that this configuration is `True` by default. When this configuration is enabled, one epoch returns the result of only one network. You are advised to change the value to `False` during debugging. + +When `dataset_sink_mode=True` is set, the result of setting the profiler is as follows: + +![resnet_profiler9](https://gitee.com/mindspore/docs/blob/master/docs/mindspore/source_zh_cn/migration_guide/images/resnet_profiler9.png) + +The execution time is reduced by half. + +Let's go on with the analysis and optimization. According to the execution time of forward and backward operators, `Cast` and `BatchNorm` account for almost 50%. Why are there so many `Cast`? According to [Constructing MindSpore Network](https://www.mindspore.cn/docs/en/master/migration_guide/model_development/model_development.html), Conv, Sort, and TopK in the Ascend environment can only be float16. Therefore, the `Cast` operator is added before and after Conv calculation. The most direct method is to change the network calculation to float16. Only `Cast` is added before the network input and loss computation. The consumption of the `Cast` operator can be ignored. This involves the mixed precision policy of MindSpore. + +MindSpore has three methods to use mixed precision: + +1. Use `Cast` to convert the network input `cast` into `float16` and the loss input `cast` into `float32`. +2. Use the `to_float` method of `Cell`. For details, see [Network Entity and Loss Construction](https://www.mindspore.cn/docs/en/master/migration_guide/model_development/model_and_loss.html). +3. Use the `amp_level` interface of the `Model` to perform mixed precision. For details, see [Automatic Mixed-Precision](https://www.mindspore.cn/tutorials/experts/en/master/others/mixed_precision.html#automatic-mixed-precision). + +Use the third method to set `amp_level` in `Model` to `O3` and check the profiler result. + +![resnet_profiler10](https://gitee.com/mindspore/docs/blob/master/docs/mindspore/source_zh_cn/migration_guide/images/resnet_profiler10.png) + +Each step takes only 23 ms. + +Finally, let's look at data processing. + +![resnet_profiler11](https://gitee.com/mindspore/docs/blob/master/docs/mindspore/source_zh_cn/migration_guide/images/resnet_profiler11.png) + +After the sink mode is added, there are two queues in total. The host queue is a queue in the memory. The dataset object continuously places the input data required by the network in the host queue. +The other is a data queue on the device. The data in the host queue is cached to the data queue, and the network directly obtains the model input from the data queue. + +The host queue is empty in many places, indicating that the dataset is quickly taken away by the data queue when data is continuously generated. The data queue is almost full. Therefore, data can keep up with network training, and data processing is not the bottleneck of network training. + +If most of the data queues are empty, you need to optimize the data performance. For example: + +![resnet_profiler12](https://gitee.com/mindspore/docs/blob/master/docs/mindspore/source_zh_cn/migration_guide/images/resnet_profiler12.png) + +In the queue of each data processing operation, the last operator and the `batch` operator are empty for a long time. In this case, you can increase the degree of parallelism of the `batch` operator. For details, see [Data Processing Performance Tuning](https://www.mindspore.cn/tutorials/experts/en/master/dataset/optimize.html). + +The code required for ResNet migration can be obtained from [code](https://gitee.com/mindspore/docs/tree/master/docs/mindspore/source_zh_cn/migration_guide/code). + +You can click the following video to learn. + +

+ +
diff --git a/docs/mindspore/source_en/migration_guide/typical_api_comparision.md b/docs/mindspore/source_en/migration_guide/typical_api_comparision.md new file mode 100644 index 0000000000..697605f054 --- /dev/null +++ b/docs/mindspore/source_en/migration_guide/typical_api_comparision.md @@ -0,0 +1,396 @@ +# Differences Between MindSpore and PyTorch + + + +## Differences Between MindSpore and PyTorch APIs + +### torch.device + +When building a model, PyTorch usually uses torch.device to specify the device to which the model and data are bound, that is, whether the device is on the CPU or GPU. If multiple GPUs are supported, you can also specify the GPU sequence number. After binding a device, you need to deploy the model and data to the device. The code is as follows: + +```python +import os +import torch +from torch import nn + +# bind to the GPU 0 if GPU is available, otherwise bind to CPU +device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") # Single GPU or CPU +# deploy model to specified hardware +model.to(device) +# deploy data to specified hardware +data.to(device) + +# distribute training on multiple GPUs +if torch.cuda.device_count() > 1: + model = nn.DataParallel(model, device_ids=[0,1,2]) + model.to(device) + + # set available device + os.environ['CUDA_VISIBLE_DEVICE']='1' + model.cuda() +``` + +In MindSpore, the `device_target` parameter in context specifies the device bound to the model, and the `device_id parameter` specifies the device sequence number. Different from PyTorch, once the device is successfully set, the input data and model are copied to the specified device for execution by default. You do not need to and cannot change the type of the device where the data and model run. The sample code is as follows: + +```python +import mindspore as ms +ms.set_context(device_target='Ascend', device_id=0) + +# define net +Model = .. +# define dataset +dataset = .. +# training, automatically deploy to Ascend according to device_target +Model.train(1, dataset) +``` + +In addition, the `Tensor` returned after the network runs is copied to the CPU device by default. You can directly access and modify the `Tensor`, including converting the `Tensor` to the `numpy` format. Unlike PyTorch, you do not need to run the `tensor.cpu` command and then convert the `Tensor` to the NumPy format. + +### nn.Module + +When PyTorch is used to build a network structure, the `nn.Module` class is used. Generally, network elements are defined and initialized in the `__init__` function, and the graph structure expression of the network is defined in the `forward` function. Objects of these classes are invoked to build and train the entire model. `nn.Module` not only provides us with graph building interfaces, but also provides us with some common [APIs](https://pytorch.org/docs/stable/generated/torch.nn.Module.html) to help us execute more complex logic. + +The `nn.Cell` class in MindSpore plays the same role as the `nn.Module` class in PyTorch. Both classes are used to build graph structures. MindSpore also provides various [APIs](https://www.mindspore.cn/docs/en/master/api_python/nn/mindspore.nn.Cell.html) for developers. Although the names are not the same, the mapping of common functions in `nn.Module` can be found in `nn.Cell`. + +The following uses several common methods as examples: + +|Common Method| nn.Module|nn.Cell| +|:----|:----|:----| +|Obtain child elements.|named_children|cells_and_names| +|Add subelements.|add_module|insert_child_to_cell| +|Obtain parameters of an element.|parameters|get_parameters| + +### Data Size + +In PyTorch, there are four types of objects that can store data: `Tensor`, `Variable`, `Parameter`, and `Buffer`. The default behaviors of the four types of objects are different. When the gradient is not required, the `Tensor` and `Buffer` data objects are used. When the gradient is required, the `Variable` and `Parameter` data objects are used. When PyTorch designs the four types of data objects, the functions are redundant. (In addition, `Variable` will be discarded.) + +MindSpore optimizes the data object design logic and retains only two types of data objects: `Tensor` and `Parameter`. The `Tensor` object only participates in calculation and does not need to perform gradient derivation or parameter update on it. The `Parameter` data object has the same meaning as the `Parameter` data object of PyTorch. The `requires_grad` attribute determines whether to perform gradient derivation or parameter update on the `Parameter` data object. During network migration, all data objects that are not updated in PyTorch can be declared as `Tensor` in MindSpore. + +### Gradient Derivation + +The operator and interface differences involved in gradient derivation are mainly caused by different automatic differentiation principles of MindSpore and PyTorch. + +### torch.no_grad + +In PyTorch, by default, information required for backward propagation is recorded when forward computation is performed. In the inference phase or in a network where backward propagation is not required, this operation is redundant and time-consuming. Therefore, PyTorch provides `torch.no_grad` to cancel this process. + +MindSpore constructs a backward graph based on the forward graph structure only when `GradOperation` is invoked. No information is recorded during forward execution. Therefore, MindSpore does not need this interface. It can be understood that forward calculation of MindSpore is performed in `torch.no_grad` mode. + +### retain_graph + +PyTorch is function-based automatic differentiation. Therefore, by default, the recorded information is automatically cleared after each backward propagation is performed for the next iteration. As a result, when we want to reuse the backward graph and gradient information, the information fails to be obtained because it has been deleted. Therefore, PyTorch provides `backward(retain_graph=True)` to proactively retain the information. + +MindSpore does not require this function. MindSpore is an automatic differentiation based on the computational graph. The backward graph information is permanently recorded in the computational graph after `GradOperation` is invoked. You only need to invoke the computational graph again to obtain the gradient information. + +### High-order Derivatives + +Automatic differentiation based on computational graphs also has an advantage that we can easily implement high-order derivation. After the `GradOperation` operation is performed on the forward graph for the first time, a first-order derivative may be obtained. In this case, the computational graph is updated to a backward graph structure of the forward graph + the first-order derivative. However, after the `GradOperation` operation is performed on the updated computational graph again, a second-order derivative may be obtained, and so on. Through automatic differentiation based on computational graph, we can easily obtain the higher order derivative of a network. + +## Differences Between MindSpore and PyTorch Operators + +Most operators and APIs of MindSpore are similar to those of TensorFlow, but the default behavior of some operators is different from that of PyTorch or TensorFlow. During network script migration, if developers do not notice these differences and directly use the default behavior, the network may be inconsistent with the original migration network, affecting network training. It is recommended that developers align not only the used operators but also the operator attributes during network migration. Here we summarize several common difference operators. + +### nn.Dropout + +Dropout is often used to prevent training overfitting. It has an important probability value parameter. The meaning of this parameter in MindSpore is completely opposite to that in PyTorch and TensorFlow. + +In MindSpore, the probability value corresponds to the `keep_prob` attribute of the Dropout operator, indicating the probability that the input is retained. `1-keep_prob` indicates the probability that the input is set to 0. + +In PyTorch and TensorFlow, the probability values correspond to the attributes `p` and `rate` of the Dropout operator, respectively. They indicate the probability that the input is set to 0, which is opposite to the meaning of `keep_prob` in MindSpore.nn.Dropout. + +For more information, visit [MindSpore Dropout](https://www.mindspore.cn/docs/en/master/api_python/nn/mindspore.nn.Dropout.html#mindspore.nn.Dropout), [PyTorch Dropout](https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html), and [TensorFlow Dropout](https://www.tensorflow.org/api_docs/python/tf/nn/dropout). + +### nn.BatchNorm2d + +BatchNorm is a special regularization method in the CV field. It has different computation processes during training and inference and is usually controlled by operator attributes. BatchNorm of MindSpore and PyTorch uses two different parameter groups at this point. + +- Difference 1 + +`torch.nn.BatchNorm2d` status under different parameters + +|training|track_running_stats|Status| +|:----|:----|:--------------------------------------| +|True|True|Expected training status. `running_mean` and `running_var` trace the statistical features of the batch in the entire training process. Each group of input data is normalized based on the mean and var statistical features of the current batch, and then `running_mean` and `running_var` are updated.| +|True|False|Each group of input data is normalized based on the statistics feature of the current batch, but the `running_mean` and `running_var` parameters do not exist.| +|False|True|Expected inference status. The BN uses `running_mean` and `running_var` for normalization and does not update them.| +|False|False|The effect is the same as that of the second status. The only difference is that this is the inference status and does not learn the weight and bias parameters. Generally, this status is not used.| + +`mindspore.nn.BatchNorm2d` status under different parameters + +|use_batch_statistics|Status| +|:----|:--------------------------------------| +|True|Expected training status. `moving_mean` and `moving_var` trace the statistical features of the batch in the entire training process. Each group of input data is normalized based on the mean and var statistical features of the current batch, and then `moving_mean` and `moving_var` are updated. +|Fasle|Expected inference status. The BN uses `moving_mean` and `moving_var` for normalization and does not update them. +|None|`use_batch_statistics` is automatically set. For training, set `use_batch_statistics` to `True`. For inference, `set use_batch_statistics` to `False`. + +Compared with `torch.nn.BatchNorm2d`, `mindspore.nn.BatchNorm2d` does not have two redundant states and retains only the most commonly used training and inference states. + +- Difference 2 + +The meaning of the momentum parameter of the BatchNorm series operators in MindSpore is opposite to that in PyTorch. The relationship is as follows: + +$$momentum_{pytorch} = 1 - momentum_{mindspore}$$ + +References: [mindspore.nn.BatchNorm2d](https://www.mindspore.cn/docs/en/master/api_python/nn/mindspore.nn.BatchNorm2d.html), [torch.nn.BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) + +### ops.Transpose + +During axis transformation, PyTorch usually uses two operators: `Tensor.permute` and `torch.transpose`. MindSpore and TensorFlow provide only the `transpose` operator. Note that the `Tensor.permute` contains the functions of the `torch.transpose`. The `torch.transpose` supports only the exchange of two axes at the same time, whereas the `Tensor.permute` supports the exchange of multiple axes at the same time. + +```python +# PyTorch code +import numpy as np +import torch +from torch import Tensor +data = np.empty((1, 2, 3, 4)).astype(np.float32) +ret1 = torch.transpose(Tensor(data), dim0=0, dim1=3) +print(ret1.shape) +ret2 = Tensor(data).permute((3, 2, 1, 0)) +print(ret2.shape) +``` + +```text + torch.Size([4, 2, 3, 1]) + torch.Size([4, 3, 2, 1]) +``` + +The `transpose` operator of MindSpore has the same function as that of TensorFlow. Although the operator is named `transpose`, it can transform multiple axes at the same time, which is equivalent to `Tensor.permute`. Therefore, MindSpore does not provide operators similar to `torch.tranpose`. + +```python +# MindSpore code +import mindspore as ms +import numpy as np +from mindspore import ops +ms.set_context(device_target="CPU") +data = np.empty((1, 2, 3, 4)).astype(np.float32) +ret = ops.Transpose()(ms.Tensor(data), (3, 2, 1, 0)) +print(ret.shape) +``` + +```text + (4, 3, 2, 1) +``` + +For more information, visit [MindSpore Transpose](https://www.mindspore.cn/docs/en/master/api_python/ops/mindspore.ops.Transpose.html#mindspore.ops.Transpose), [PyTorch Transpose](https://pytorch.org/docs/stable/generated/torch.transpose.html), [PyTorch Permute](https://pytorch.org/docs/stable/generated/torch.Tensor.permute.html), and [TensforFlow Transpose](https://www.tensorflow.org/api_docs/python/tf/transpose). + +### Conv and Pooling + +For operators similar to convolution and pooling, the size of the output feature map of the operator depends on variables such as the input feature map, step, kernel_size, and padding. + +If `pad_mode` is set to `valid`, the height and width of the output feature map are calculated as follows: + +![conv-formula](https://mindspore-website.obs.cn-north-4.myhuaweicloud.com/website-images/master/docs/mindspore/source_zh_cn/migration_guide/model_development/images/conv_formula.png) + +If pad_mode (corresponding to the padding attribute in PyTorch, which has a different meaning from pad_mode) is set to `same`, automatic padding needs to be performed on the input feature map sometimes. When the padding element is an even number, padding elements are evenly distributed on the top, bottom, left, and right of the feature map. In this case, the behavior of this type of operators in MindSpore, PyTorch, and TensorFlow is the same. + +However, when the padding element is an odd number, PyTorch is preferentially filled on the left and upper sides of the input feature map. + +![padding1](https://mindspore-website.obs.cn-north-4.myhuaweicloud.com/website-images/master/docs/mindspore/source_zh_cn/migration_guide/model_development/images/padding_pattern1.png) + +MindSpore and TensorFlow are preferentially filled on the right and bottom of the feature map. + +![padding2](https://mindspore-website.obs.cn-north-4.myhuaweicloud.com/website-images/master/docs/mindspore/source_zh_cn/migration_guide/model_development/images/padding_pattern2.png) + +The following is an example: + +```python +# mindspore example +import numpy as np +import mindspore as ms +from mindspore import ops +ms.set_context(device_target="CPU") +data = np.arange(9).reshape(1, 1, 3, 3).astype(np.float32) +print(data) +op = ops.MaxPool(kernel_size=2, strides=2, pad_mode='same') +print(op(ms.Tensor(data))) +``` + +```text + [[[[0. 1. 2.] + [3. 4. 5.] + [6. 7. 8.]]]] + [[[[4. 5.] + [7. 8.]]]] +``` + +During MindSpore model migration, if the PyTorch pre-training model is loaded to the model and fine-tune is performed in MindSpore, the difference may cause precision decrease. Developers need to pay special attention to the convolution whose padding policy is same. + +To keep consistent with the PyTorch behavior, you can use the `ops.Pad` operator to manually pad elements, and then use the convolution and pooling operations when `pad_mode=\"valid\"` is set. + +```python +import numpy as np +import mindspore as ms +from mindspore import ops +ms.set_context(device_target="CPU") +data = np.arange(9).reshape(1, 1, 3, 3).astype(np.float32) +# only padding on top left of feature map +pad = ops.Pad(((0, 0), (0, 0), (1, 0), (1, 0))) +data = pad(ms.Tensor(data)) +res = ops.MaxPool(kernel_size=2, strides=2, pad_mode='same')(data) +print(res) +``` + +```text + [[[[0. 2.] + [6. 8.]]]] +``` + +### Different Default Weight Initialization + +We know that weight initialization is very important for network training. Generally, each operator has an implicit declaration weight. In different frameworks, the implicit declaration weight may be different. Even if the operator functions are the same, if the implicitly declared weight initialization mode distribution is different, the training process is affected or even cannot be converged. + +Common operators that implicitly declare weights include Conv, Dense(Linear), Embedding, and LSTM. The Conv and Dense operators differ greatly. + +- Conv2d + + - mindspore.nn.Conv2d (weight: $\mathcal{N} (0, 1) $, bias: zeros) + - torch.nn.Conv2d (weight: $\mathcal{U} (-\sqrt{k},\sqrt{k} )$, bias: $\mathcal{U} (-\sqrt{k},\sqrt{k} )$) + - tf.keras.Layers.Conv2D (weight: glorot_uniform, bias: zeros) + + In the preceding information, $k=\frac{groups}{c_{in}*\prod_{i}^{}{kernel\_size[i]}}$ + +- Dense(Linear) + + - mindspore.nn.Linear (weight: $\mathcal{N} (0, 1) $, bias: zeros) + - torch.nn.Dense (weight: $\mathcal{U} (-\sqrt{k},\sqrt{k} )$, bias: $\mathcal{U} (-\sqrt{k},\sqrt{k} )$) + - tf.keras.Layers.Dense (weight: glorot_uniform, bias: zeros) + + In the preceding information, $k=\frac{groups}{in\_features}$ + +For a network without normalization, for example, a GAN network without the BatchNorm operator, the gradient is easy to explode or disappear. Therefore, weight initialization is very important. Developers should pay attention to the impact of weight initialization. + +## Differences Between MindSpore and PyTorch Execution Processes + +### Automatic Differentiation + +Both MindSpore and PyTorch provide the automatic differentiation function. After the forward network is defined, automatic backward propagation and gradient update can be implemented through simple interface invoking. However, it should be noted that MindSpore and PyTorch use different logic to build backward graphs. This difference also brings differences in API design. + +#### PyTorch Automatic Differentiation + +As we know, PyTorch is an automatic differentiation based on computation path tracing. After a network structure is defined, no backward graph is created. Instead, during the execution of the forward graph, `Variable` or `Parameter` records the backward function corresponding to each forward computation and generates a dynamic computational graph, it is used for subsequent gradient calculation. When `backward` is called at the final output, the chaining rule is applied to calculate the gradient from the root node to the leaf node. The nodes stored in the dynamic computational graph of PyTorch are actually `Function` objects. Each time an operation is performed on `Tensor`, a `Function` object is generated, which records necessary information in backward propagation. During backward propagation, the `autograd` engine calculates gradients in backward order by using the `backward` of the `Function`. You can view this point through the hidden attribute of the `Tensor`. + +For example, run the following code: + +```python +import torch +from torch.autograd import Variable +x = Variable(torch.ones(2, 2), requires_grad=True) +x = x * 2 +y = x - 1 +y.backward(x) +``` + +The gradient result of x in the process from obtaining the definition of `x` to obtaining the output `y` is automatically obtained. + +Note that the backward of PyTorch is accumulated. After the update, you need to clear the optimizer. + +#### MindSpore Automatic Differentiation + +In graph mode, MindSpore's automatic differentiation is based on the graph structure. Different from PyTorch, MindSpore does not record any information during forward computation and only executes the normal computation process (similar to PyTorch in PyNative mode). Then the question comes. If the entire forward computation is complete and MindSpore does not record any information, how does MindSpore know how backward propagation is performed? + +When MindSpore performs automatic differentiation, the forward graph structure needs to be transferred. The automatic differentiation process is to obtain backward propagation information by analyzing the forward graph. The automatic differentiation result is irrelevant to the specific value in the forward computation and is related only to the forward graph structure. Through the automatic differentiation of the forward graph, the backward propagation process is obtained. The backward propagation process is expressed through a graph structure, that is, the backward graph. The backward graph is added after the user-defined forward graph to form a final computational graph. However, the backward graph and backward operators added later are not aware of and cannot be manually added. They can only be automatically added through the interface provided by MindSpore. In this way, errors are avoided during backward graph build. + +Finally, not only the forward graph is executed, but also the graph structure contains both the forward operator and the backward operator added by MindSpore. That is, MindSpore adds an invisible `Cell` after the defined forward graph, the `Cell` is a backward operator derived from the forward graph. + +The interface that helps us build the backward graph is [GradOperation](https://www.mindspore.cn/docs/en/master/api_python/ops/mindspore.ops.GradOperation.html). + +```python +from mindspore import nn, ops + +class GradNetWrtX(nn.Cell): + def __init__(self, net): + super(GradNetWrtX, self).__init__() + self.net = net + self.grad_op = ops.GradOperation() + def construct(self, x, y): + gradient_function = self.grad_op(self.net) + return gradient_function(x, y) +``` + +According to the document introduction, GradOperation is not an operator. Its input and output are not tensors, but cells, that is, the defined forward graph and the backward graph obtained through automatic differentiation. Why is the input a graph structure? To construct a backward graph, you do not need to know the specific input data. You only need to know the structure of the forward graph. With the forward graph, you can calculate the structure of the backward graph. Then, you can treat the forward graph and backward graph as a new computational graph, the new computational graph is like a function. For any group of data you enter, it can calculate not only the positive output, but also the gradient of ownership weight. Because the graph structure is fixed and does not save intermediate variables, the graph structure can be invoked repeatedly. + +Similarly, when we add an optimizer structure to the network, the optimizer also adds optimizer-related operators. That is, we add optimizer operators that are not perceived to the computational graph. Finally, the computational graph is built. + +In MindSpore, most operations are finally converted into real operator operations and finally added to the computational graph. Therefore, the number of operators actually executed in the computational graph is far greater than the number of operators defined at the beginning. + +MindSpore provides the [TrainOneStepCell](https://www.mindspore.cn/docs/en/master/api_python/nn/mindspore.nn.TrainOneStepCell.html) and [TrainOneStepWithLossScaleCell](https://www.mindspore.cn/docs/en/master/api_python/nn/mindspore.nn.TrainOneStepWithLossScaleCell.html) APIs to package the entire training process. If other operations, such as gradient cropping, specification, and intermediate variable return, are performed in addition to the common training process, you need to customize the training cell. For details, see [Inference and Training Process](https://www.mindspore.cn/docs/en/master/migration_guide/model_development/training_and_evaluation_procession.html). + +### Learning Rate Update + +#### PyTorch Learning Rate (LR) Update Policy + +PyTorch provides the `torch.optim.lr_scheduler` package for dynamically modifying LR. When using the package, you need to explicitly call `optimizer.step()` and `scheduler.step()` to update LR. For details, see [How Do I Adjust the Learning Rate](https://pytorch.org/docs/1.12/optim.html#how-to-adjust-learning-rate). + +#### MindSpore Learning Rate Update Policy + +The learning rate of MindSpore is packaged in the optimizer. Each time the optimizer is invoked, the learning rate update step is automatically updated. For details, see [Learning Rate and Optimizer](https://www.mindspore.cn/docs/en/master/migration_guide/model_development/learning_rate_and_optimizer.html). + +### Distributed Scenarios + +#### PyTorch Distributed Settings + +Generally, data parallelism is used in distributed scenarios. For details, see [DDP](https://pytorch.org/docs/1.12/notes/ddp.html). The following is an example of PyTorch: + +```python +import os +import torch +import torch.distributed as dist +import torch.multiprocessing as mp +import torch.nn as nn +import torch.optim as optim +from torch.nn.parallel import DistributedDataParallel as DDP + + +def example(rank, world_size): + # create default process group + dist.init_process_group("gloo", rank=rank, world_size=world_size) + # create local model + model = nn.Linear(10, 10).to(rank) + # construct DDP model + ddp_model = DDP(model, device_ids=[rank]) + # define loss function and optimizer + loss_fn = nn.MSELoss() + optimizer = optim.SGD(ddp_model.parameters(), lr=0.001) + + # forward pass + outputs = ddp_model(torch.randn(20, 10).to(rank)) + labels = torch.randn(20, 10).to(rank) + # backward pass + loss_fn(outputs, labels).backward() + # update parameters + optimizer.step() + +def main(): + world_size = 2 + mp.spawn(example, + args=(world_size,), + nprocs=world_size, + join=True) + +if __name__=="__main__": + # Environment variables which need to be + # set when using c10d's default "env" + # initialization mode. + os.environ["MASTER_ADDR"] = "localhost" + os.environ["MASTER_PORT"] = "29500" + main() +``` + +#### MindSpore Distributed Settings + +The distributed configuration of MindSpore uses runtime configuration items. For details, see [Distributed Parallel Training Mode](https://www.mindspore.cn/tutorials/experts/en/master/parallel/introduction.html). For example: + +```python +import mindspore as ms +from mindspore.communication.management import init, get_rank, get_group_size + +# Initialize the multi-device environment. +init() + +# Obtain the number of devices in a distributed scenario and the logical ID of the current process. +group_size = get_group_size() +rank_id = get_rank() + +# Configure data parallel mode in distributed mode. +ms.set_auto_parallel_context(parallel_mode=ms.ParallelMode.DATA_PARALLEL, gradients_mean=True) +``` diff --git a/docs/mindspore/source_en/migration_guide/use_third_party_op.md b/docs/mindspore/source_en/migration_guide/use_third_party_op.md index cce57f14c9..619701b916 100644 --- a/docs/mindspore/source_en/migration_guide/use_third_party_op.md +++ b/docs/mindspore/source_en/migration_guide/use_third_party_op.md @@ -1,6 +1,6 @@ # Using Third-party Operator Libraries Based on Customized Interfaces - + ## Overview diff --git a/docs/mindspore/source_zh_cn/migration_guide/enveriment_preparation.md b/docs/mindspore/source_zh_cn/migration_guide/enveriment_preparation.md index eda34aba04..c0313f8909 100644 --- a/docs/mindspore/source_zh_cn/migration_guide/enveriment_preparation.md +++ b/docs/mindspore/source_zh_cn/migration_guide/enveriment_preparation.md @@ -58,7 +58,7 @@ ModelArts是华为云提供的面向AI开发者的一站式开发平台,集成 OBS:也叫做S3桶,在开发环境和训练环境中需要将存储在OBS上的代码、数据、预训练模型先传到对应的物理机上,之后才能执行作业。[本地上传文件到OBS](https://bbs.huaweicloud.com/blogs/212453)。 -[MoXing](https://bbs.huaweicloud.com/blogs/101129): MoXing是华为云深度学习服务提供的网络模型开发API,使用上需要重点关注数据拷贝的接口: +[MoXing](https://bbs.huaweicloud.com/blogs/101129):MoXing是华为云深度学习服务提供的网络模型开发API,使用上需要重点关注数据拷贝的接口: ```python import moxing as mox -- Gitee