1. 大幅度提升 Pytorch 的訓練速度
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") torch.backends.cudnn.benchmark = True
但加了這一行,似乎運行結果可能會存在細微差異,由於隨機數種子帶來的不同。
2. 把原有的記錄文件加個后綴變為 .bak 文件,避免直接覆蓋
# from co-teaching train code
txtfile = save_dir + "/" + model_str + "_%s.txt"%str(args.optimizer) ## good job! nowTime=datetime.datetime.now().strftime('%Y-%m-%d-%H:%M:%S') if os.path.exists(txtfile): os.system('mv %s %s' % (txtfile, txtfile+".bak-%s" % nowTime)) # bakeup 備份文件
3. 計算 Accuracy 返回list, 調用函數時,直接提取值,而非提取list
# from co-teaching code but MixMatch_pytorch code also has it
def accuracy(logit, target, topk=(1,)): """Computes the precision@k for the specified values of k""" output = F.softmax(logit, dim=1) # but actually not need it maxk = max(topk) batch_size = target.size(0) _, pred = output.topk(maxk, 1, True, True) # _, pred = logit.topk(maxk, 1, True, True) pred = pred.t() correct = pred.eq(target.view(1, -1).expand_as(pred)) res = [] for k in topk: correct_k = correct[:k].view(-1).float().sum(0, keepdim=True) res.append(correct_k.mul_(100.0 / batch_size)) # it seems this is a bug, when not all batch has same size, the mean of accuracy of each batch is not the mean of accu of all dataset return res prec1, = accuracy(logit, labels, topk=(1,)) # , indicate tuple unpackage prec1, prec5 = accuracy(logits, labels, topk=(1, 5))
4. 善於利用 logger 文件來記錄每一個 epoch 的實驗值
# from Pytorch_MixMatch code
class Logger(object): '''Save training process to log file with simple plot function.''' def __init__(self, fpath, title=None, resume=False): self.file = None self.resume = resume self.title = '' if title == None else title if fpath is not None: if resume: self.file = open(fpath, 'r') name = self.file.readline() self.names = name.rstrip().split('\t') self.numbers = {} for _, name in enumerate(self.names): self.numbers[name] = [] for numbers in self.file: numbers = numbers.rstrip().split('\t') for i in range(0, len(numbers)): self.numbers[self.names[i]].append(numbers[i]) self.file.close() self.file = open(fpath, 'a') else: self.file = open(fpath, 'w') def set_names(self, names): if self.resume: pass # initialize numbers as empty list self.numbers = {} self.names = names for _, name in enumerate(self.names): self.file.write(name) self.file.write('\t') self.numbers[name] = [] self.file.write('\n') self.file.flush() def append(self, numbers): assert len(self.names) == len(numbers), 'Numbers do not match names' for index, num in enumerate(numbers): self.file.write("{0:.4f}".format(num)) self.file.write('\t') self.numbers[self.names[index]].append(num) self.file.write('\n') self.file.flush() def plot(self, names=None): names = self.names if names == None else names numbers = self.numbers for _, name in enumerate(names): x = np.arange(len(numbers[name])) plt.plot(x, np.asarray(numbers[name])) plt.legend([self.title + '(' + name + ')' for name in names]) plt.grid(True) def close(self): if self.file is not None: self.file.close() # usage logger = Logger(new_folder+'/log_for_%s_WebVision1M.txt'%data_type, title=title) logger.set_names(['epoch', 'val_acc', 'val_acc_ImageNet']) for epoch in range(100): logger.append([epoch, val_acc, val_acc_ImageNet]) logger.close()
也可以使用如下方法記錄, (2020.12.15) 利用到 pandas.DataFrame:
#來自 https://colab.research.google.com/github/facebookresearch/moco/blob/colab-notebook/colab/moco_cifar10_demo.ipynb#scrollTo=dvuxcmejkKt8 # logging results = {'train_loss': [], 'test_acc@1': []} if not os.path.exists(args.results_dir): os.mkdir(args.results_dir) # dump args with open(args.results_dir + '/args.json', 'w') as fid: json.dump(args.__dict__, fid, indent=2) # training loop for epoch in range(epoch_start, args.epochs + 1): train_loss = train(model, train_loader, optimizer, epoch, args) results['train_loss'].append(train_loss) test_acc_1 = test(model.encoder_q, memory_loader, test_loader, epoch, args) results['test_acc@1'].append(test_acc_1) # guixj append print("epoch: %d"%epoch, "test_acc_1:", test_acc_1) # save statistics data_frame = pd.DataFrame(data=results, index=range(epoch_start, epoch + 1)) data_frame.to_csv(args.results_dir + '/log.csv', index_label='epoch')
5. 利用 argparser 命令行工具來進行代碼重構,使用不同參數適配不同數據集,不同優化方式,不同setting, 避免多個高度冗余的重復代碼
# argparser 命令行工具有一個坑的地方是,無法設置 bool 變量, --flag FALSE, 然后會解釋為 字符串,仍然當做 True
但最近發現可以使用如下命令來進行修補,來自 ICML-19-SGC github 上代碼
1 parser.add_argument('--test', action='store_true', default=False, help='inductive training.')
- 當命令行出現 test 字樣時,則為 args.test = true
- 若未出現 test 字樣,則為 args.test = false
import argparse # from https://github.com/Diego999/pyGAT/blob/master/train.py parser = argparse.ArgumentParser() parser.add_argument('--no-cuda', action='store_true', default=False, help='Disables CUDA training.') parser.add_argument('--fastmode', action='store_true', default=False, help='Validate during training pass.') parser.add_argument('--sparse', action='store_true', default=False, help='GAT with sparse version or not.') parser.add_argument('--seed', type=int, default=72, help='Random seed.') parser.add_argument('--epochs', type=int, default=10000, help='Number of epochs to train.') parser.add_argument('--lr', type=float, default=0.005, help='Initial learning rate.') parser.add_argument('--weight_decay', type=float, default=5e-4, help='Weight decay (L2 loss on parameters).') parser.add_argument('--hidden', type=int, default=8, help='Number of hidden units.') parser.add_argument('--nb_heads', type=int, default=8, help='Number of head attentions.') parser.add_argument('--dropout', type=float, default=0.6, help='Dropout rate (1 - keep probability).') parser.add_argument('--alpha', type=float, default=0.2, help='Alpha for the leaky_relu.') parser.add_argument('--patience', type=int, default=100, help='Patience') args = parser.parse_args()
args.cuda = not args.no_cuda and torch.cuda.is_available() # args.cuda 是可以臨時定義的,不過需要定義在 parser.parse_args() 后面
# 也出現在 https://colab.research.google.com/github/facebookresearch/moco/blob/colab-notebook/colab/moco_cifar10_demo.ipynb#scrollTo=dvuxcmejkKt8
# 來自 https://github.com/PatrickHua/SimSiam/blob/main/arguments.py 的高級用法
vars(args)['aug_kwargs'] = { # 增加一個參數值對象,非常高級的用法 'name':args.model.name, 'image_size': args.dataset.image_size }
6. 使用shell 變量來設置所使用的顯卡, 便於利用shell 腳本進行程序的串行,從而掛起來跑。或者多開幾個 screen 進行同一張卡上多個程序並行跑,充分利用顯卡的內存。
命令行中使用如下語句,或者把語句寫在 shell 腳本中 # 不要忘了 export
export CUDA_VISIBLE_DEVICES=1 #設置當前可用顯卡為編號為1的顯卡(從 0 開始編號),即不在 0 號上跑 export CUDA_VISIBlE_DEVICES=0,1 # 設置當前可用顯卡為 0,1 顯卡,當 0 用滿后,就會自動使用 1 顯卡
一般經驗,即使多個程序並行跑時,即使顯存完全足夠,單個程序的速度也會變慢,這可能是由於還有 cpu 和內存的限制。
這里顯存占用不是阻礙,應該主要看GPU 利用率(也就是計算單元的使用,如果達到了 99% 就說明程序過多了。)
使用 watch nvidia-smi 來監測每個程序當前是否在正常跑。
7. 使用 python 時間戳來保存並進行區別不同的 result 文件
- 參照自己很早之前寫的 co-training 的代碼
- 參考 MoCoV2 的代碼
if args.results_dir == '': args.results_dir = './cache-' + datetime.now().strftime("%Y-%m-%d-%H-%M-%S-moco")
8. 把訓練時命令行窗口的 print 輸出全部保存到一個 log 文件:(參照 DIEN 代碼)
mkdir dnn_save_path mkdir dnn_best_model CUDA_VISIBLE_DEVICES=0
/usr/bin/python2.7 script/train.py train DIEN >train_dien2.log 2>&1 & # 最后一個 & 似乎是讓進程在后台運行,參見《Linux命令行大全》第10章
這里的 2>&1 表示把 標准錯誤 導出到 標准輸出 中, 與前面的 > train_dien2.log 組合起來則相當於把 python 程序的標准輸出和標准錯誤都導入到文件 train_dien2.log 中。
並且使用如下命令 | tee 命令則可以同時保存到文件並且寫到命令行輸出:
python script/train.py train DIEN | tee train_dein2.log
PS:hanzy 最早就是用此來記錄實驗結果,我可能在實習之前就學會了此,應該是在之前的某個代碼庫里有此。跑類別不平衡代碼就已經會此。
9. git clone 可以用來下載 github 上的代碼,更快。(由 DIEN 的下載)
git clone https://github.com/mouna99/dien.git 使用這個命令可以下載 github 上的代碼庫
10. (來自 DIEN ) 對於命令行參數不一定要使用 argparser 來讀取,也可以直接使用 sys.argv 讀取,不過這樣的話,就無法指定關鍵字參數,只能使用位置參數。
1 ### run.sh ### 2 CUDA_VISIBLE_DEVICES=0 /usr/bin/python2.7 script/train.py train DIEN >train_dein2.log 2>&1 & 3 ############# 4 5 if __name__ == '__main__': 6 if len(sys.argv) == 4: 7 SEED = int(sys.argv[3]) # 0,1,2,3 8 else: 9 SEED = 3 10 tf.set_random_seed(SEED) 11 numpy.random.seed(SEED) 12 random.seed(SEED) 13 if sys.argv[1] == 'train': 14 train(model_type=sys.argv[2], seed=SEED) 15 elif sys.argv[1] == 'test': 16 test(model_type=sys.argv[2], seed=SEED) 17 else: 18 print('do nothing...')
參考 https://blog.csdn.net/dcrmg/article/details/51987413
argc 是 argument count 的縮寫,表示傳入main函數的參數個數;
argv 是 argument vector 的縮寫,表示傳入main函數的參數序列或指針,並且第一個參數argv[0]一定是程序的名稱,並且包含了程序所在的完整路徑,所以確切的說需要我們輸入的main函數的參數個數應該是argc - 1個;
11.代碼的一種邏輯:time_point 是一個參數變量,可以有兩種方案來處理
一種直接在外面判斷:
1 #適用於輸出變量的個數不同的情況 2 if time_point: 3 A, B, C = f1(x, y, time_point=True) 4 else: 5 A, B = f1(x, y, time_point=False) 6 7 # 適用於輸出變量個數和類型相同的情況 8 9 C, D = f2(x, y, time_point=time_point)
12. 寫一個 shell 腳本文件來進行調節超參數, 來自 [NIPS-20 Grand]
mkdir cora for num in $(seq 0 99)
do python train_grand.py --hidden 32 --lr 0.01 --patience 200 --seed $num --dropnode_rate 0.5 > cora/"$num".txt done
13. 使用 或者 不使用 cuda 運行結果可能會不一樣,有細微差別。
cuda 也有一個相關的隨機數種子的參數,當不使用 cuda 時,這一個隨機數種子沒有起到作用,因此可能會得到不同的結果。
來自 NIPS-20 Grand (2020.11.18)的實驗結果發現。
14. 在 ipython 交互式界面中是無法使用讀取命令行參數的,需要使用一個空字符讀取默認值:
''' # https://colab.research.google.com/github/facebookresearch/moco/blob/colab-notebook/colab/moco_cifar10_demo.ipynb#scrollTo=dvuxcmejkKt8 args = parser.parse_args() # running in command line, 為空也讀取默認值 ''' args = parser.parse_args('') # running in ipynb, 全部會使用默認值
15. 保存模型的所有超參數:來自 MoCo_CIFAR10
print(args) # load model if resume epoch_start = 1 if args.resume is not '': checkpoint = torch.load(args.resume) model.load_state_dict(checkpoint['state_dict']) optimizer.load_state_dict(checkpoint['optimizer']) epoch_start = checkpoint['epoch'] + 1 print('Loaded from: {}'.format(args.resume)) # logging results = {'train_loss': [], 'test_acc@1': []} if not os.path.exists(args.results_dir): os.mkdir(args.results_dir) # dump args with open(args.results_dir + '/args.json', 'w') as fid: json.dump(args.__dict__, fid, indent=2)
先前的 LDAM 的代碼似乎也記錄了很多,幾乎全部把所有的實驗設置全部都記錄下來了,后面有空可以看一下。
16. 利用 tqdm 的 bar, 似乎還有一個 progress 也有類似的功能?
# MoCo 代碼
from tqdm import tqdm def train(): train_bar = tqdm(data_loader) for im_1, im_2 in train_bar: loss = net(im_1, im_2) train_optimizer.zero_grad() loss.backward() train_optimizer.step() total_num += data_loader.batch_size total_loss += loss.item() * data_loader.batch_size train_bar.set_description('Train Epoch: [{}/{}], lr: {:.6f}, Loss: {:.4f}'.format(epoch, args.epochs, optimizer.param_groups[0]['lr'], total_loss / total_num)) return total_loss / total_num
# SimSiam 代碼也用此
17. 最近看到 SimSiam 的實現也用到 AverageMeter() 函數,所以准備記錄一下。google 搜索發現 MixMatch 代碼庫的作者也用了此,並給出了出處,原來是參考 pytorch 官方訓練 ImageNet 的代碼,看來這些人都讀過非常優秀的 pytorch 官方源代碼庫,所以能夠寫出這么簡潔有力的代碼。
class AverageMeter(object): """Computes and stores the average and current value Imported from https://github.com/pytorch/examples/blob/master/imagenet/main.py#L247-L262 """ def __init__(self): self.reset() def reset(self): self.val = 0 self.avg = 0 self.sum = 0 self.count = 0 def update(self, val, n=1): self.val = val self.sum += val * n self.count += n self.avg = self.sum / self.count
不過一些代碼庫關於AverageMeter 的用法有一些錯誤,當前的用法是:
acc_meter = AverageMeter(name='Accuracy') acc_meter.reset() for images, labels, indices in testloader: with torch.no_grad(): feature = model(images.to(args.device)) preds = classifier(feature).argmax(dim=1) correct = (preds == labels.to(args.device)).sum().item() acc_meter.update(correct/preds.shape[0]) # 這里讀入每個 batch 的平均后的 accuracy print(f'Accuracy = {acc_meter.avg*100:.2f}')
注意到,例如設置 batch_size = 128, 但一般測試集的大小無法恰好整除 128, 就會導致最后一個 batch 樣本數小於 128,這樣直接計算 batch 的平均后的 accuracy, 再進行平均,算出來的值有問題。除非 test_loader 設置了 drop_last=True, 把最后一個大小不為 128 的 batch 直接丟掉,但這樣不合理,相當於改變了 test_loader。一般只會在 train_loader 設置 drop_last=True。
修復方法例如,可以改為如下代碼:
acc_meter.update(correct, preds.shape[0])
The End.