aiface's picture
Upload 8 files
06d8add
raw
history blame
19.8 kB
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2020 Imperial College London (Pingchuan Ma)
# Apache 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
""" TCN for lipreading"""
import os
import time
import random
import argparse # 명령행 인자를 파싱해주는 모듈
import numpy as np
from tqdm import tqdm # 작업진행률 표시하는 라이브러리
import torch # 파이토치
import torch.nn as nn # 클래스 # attribute 를 활용해 state 를 저장하고 활용
import torch.nn.functional as F # 함수 # 인스턴스화시킬 필요없이 사용 가능
from lipreading.utils import get_save_folder
from lipreading.utils import load_json, save2npz
from lipreading.utils import load_model, CheckpointSaver
from lipreading.utils import get_logger, update_logger_batch
from lipreading.utils import showLR, calculateNorm2, AverageMeter
from lipreading.model import Lipreading
from lipreading.mixup import mixup_data, mixup_criterion
from lipreading.optim_utils import get_optimizer, CosineScheduler
from lipreading.dataloaders import get_data_loaders, get_preprocessing_pipelines
from pathlib import Path
import wandb # 학습 관리 툴 (Loss, Acc 자동 저장)
# 인자값을 받아서 처리하는 함수
def load_args(default_config=None):
# 인자값을 받을 수 있는 인스턴스 생성
parser = argparse.ArgumentParser(description='Pytorch Lipreading ')
# 입력받을 인자값 목록
# -- dataset config
parser.add_argument('--dataset', default='lrw', help='dataset selection')
parser.add_argument('--num-classes', type=int, default=30, help='Number of classes')
parser.add_argument('--modality', default='video', choices=['video', 'raw_audio'], help='choose the modality')
# -- directory
parser.add_argument('--data-dir', default='./datasets/visual', help='Loaded data directory')
parser.add_argument('--label-path', type=str, default='./labels/30VietnameseSort.txt', help='Path to txt file with labels')
parser.add_argument('--annonation-direc', default=None, help='Loaded data directory')
# -- model config
parser.add_argument('--backbone-type', type=str, default='resnet', choices=['resnet', 'shufflenet'], help='Architecture used for backbone')
parser.add_argument('--relu-type', type=str, default='relu', choices=['relu','prelu'], help='what relu to use' )
parser.add_argument('--width-mult', type=float, default=1.0, help='Width multiplier for mobilenets and shufflenets')
# -- TCN config
parser.add_argument('--tcn-kernel-size', type=int, nargs="+", help='Kernel to be used for the TCN module')
parser.add_argument('--tcn-num-layers', type=int, default=4, help='Number of layers on the TCN module')
parser.add_argument('--tcn-dropout', type=float, default=0.2, help='Dropout value for the TCN module')
parser.add_argument('--tcn-dwpw', default=False, action='store_true', help='If True, use the depthwise seperable convolution in TCN architecture')
parser.add_argument('--tcn-width-mult', type=int, default=1, help='TCN width multiplier')
# -- train
parser.add_argument('--training-mode', default='tcn', help='tcn')
parser.add_argument('--batch-size', type=int, default=8, help='Mini-batch size') # dafault=32 에서 default=8 (OOM 방지) 로 변경
parser.add_argument('--optimizer',type=str, default='adamw', choices = ['adam','sgd','adamw'])
parser.add_argument('--lr', default=3e-4, type=float, help='initial learning rate')
parser.add_argument('--init-epoch', default=0, type=int, help='epoch to start at')
parser.add_argument('--epochs', default=100, type=int, help='number of epochs') # dafault=80 에서 default=10 (테스트 용도) 로 변경
parser.add_argument('--test', default=False, action='store_true', help='training mode')
parser.add_argument('--save-dir', type=Path, default=Path('/kaggle/working/result/'))
# -- mixup
parser.add_argument('--alpha', default=0.4, type=float, help='interpolation strength (uniform=1., ERM=0.)')
# -- test
parser.add_argument('--model-path', type=str, default=None, help='Pretrained model pathname')
parser.add_argument('--allow-size-mismatch', default=False, action='store_true',
help='If True, allows to init from model with mismatching weight tensors. Useful to init from model with diff. number of classes')
# -- feature extractor
parser.add_argument('--extract-feats', default=False, action='store_true', help='Feature extractor')
parser.add_argument('--mouth-patch-path', type=str, default=None, help='Path to the mouth ROIs, assuming the file is saved as numpy.array')
parser.add_argument('--mouth-embedding-out-path', type=str, default=None, help='Save mouth embeddings to a specificed path')
# -- json pathname
parser.add_argument('--config-path', type=str, default=None, help='Model configuration with json format')
# -- other vars
parser.add_argument('--interval', default=50, type=int, help='display interval')
parser.add_argument('--workers', default=2, type=int, help='number of data loading workers') # dafault=8 에서 default=2 (GCP core 4개의 절반) 로 변경
# paths
parser.add_argument('--logging-dir', type=str, default='/kaggle/working/train_logs', help = 'path to the directory in which to save the log file')
# 입력받은 인자값을 args에 저장 (type: namespace)
args = parser.parse_args()
return args
args = load_args() # args 파싱 및 로드
# 실험 재현을 위해서 난수 고정
torch.manual_seed(1) # 메인 프레임워크인 pytorch 에서 random seed 고정
np.random.seed(1) # numpy 에서 random seed 고정
random.seed(1) # python random 라이브러리에서 random seed 고정
# 참고: 실험 재현하려면 torch.backends.cudnn.deterministic = True, torch.backends.cudnn.benchmark = False 이어야 함
torch.backends.cudnn.benchmark = True # 내장된 cudnn 자동 튜너를 활성화하여, 하드웨어에 맞게 사용할 최상의 알고리즘(텐서 크기나 conv 연산에 맞게)을 찾음
# feature 추출
def extract_feats(model):
"""
:rtype: FloatTensor
"""
model.eval() # evaluation 과정에서 사용하지 않아야 하는 layer들을 알아서 off 시키도록 하는 함수
preprocessing_func = get_preprocessing_pipelines()['test'] # test 전처리
mouth_patch_path = args.mouth_patch_path.replace('.','')
dir_name = os.path.dirname(os.path.abspath(__file__))
dir_name = dir_name + mouth_patch_path
data_paths = [os.path.join(pth, f) for pth, dirs, files in os.walk(dir_name) for f in files]
npz_files = np.load(data_paths[0])['data']
data = preprocessing_func(npz_files) # data: TxHxW
# data = preprocessing_func(np.load(args.mouth_patch_path)['data']) # data: TxHxW
return data_paths[0], model(torch.FloatTensor(data)[None, None, :, :, :].cuda(), lengths=[data.shape[0]])
# return model(torch.FloatTensor(data)[None, None, :, :, :].cuda(), lengths=[data.shape[0]])
# 평가
def evaluate(model, dset_loader, criterion, is_print=False):
model.eval() # evaluation 과정에서 사용하지 않아야 하는 layer들을 알아서 off 시키도록 하는 함수
# running_loss = 0.
# running_corrects = 0.
prediction=''
# evaluation/validation 과정에선 보통 model.eval()과 torch.no_grad()를 함께 사용함
with torch.no_grad():
inferences = []
for batch_idx, (input, lengths, labels) in enumerate(tqdm(dset_loader)):
# 모델 생성
# input 텐서의 차원을 하나 더 늘리고 gpu 에 할당
logits = model(input.unsqueeze(1).cuda(), lengths=lengths)
# _, preds = torch.max(F.softmax(logits, dim=1).data, dim=1) # softmax 적용 후 각 원소 중 최대값 가져오기
# running_corrects += preds.eq(labels.cuda().view_as(preds)).sum().item() # 정확도 계산
# loss = criterion(logits, labels.cuda()) # loss 계산
# running_loss += loss.item() * input.size(0) # loss.item(): loss 가 갖고 있는 scalar 값
# # ------------ Prediction, Confidence 출력 ------------
probs = torch.nn.functional.softmax(logits, dim=-1)
probs = probs[0].detach().cpu().numpy()
label_path = args.label_path
with Path(label_path).open() as fp:
vocab = fp.readlines()
top = np.argmax(probs)
prediction = vocab[top].strip()
# confidence = np.round(probs[top], 3)
# inferences.append({
# 'prediction': prediction,
# 'confidence': confidence
# })
if is_print:
print()
print(f'Prediction: {prediction}')
# print(f'Confidence: {confidence}')
print()
return prediction
# ------------ Prediction, Confidence 텍스트 파일 저장 ------------
# txt_save_path = str(args.save_dir) + f'/predict.txt'
# # 파일 없을 경우
# if not os.path.exists(os.path.dirname(txt_save_path)):
# os.makedirs(os.path.dirname(txt_save_path)) # 디렉토리 생성
# with open(txt_save_path, 'w') as f:
# for inference in inferences:
# prediction = inference['prediction']
# confidence = inference['confidence']
# f.writelines(f'Prediction: {prediction}, Confidence: {confidence}\n')
# print('Test Dataset {} In Total \t CR: {}'.format( len(dset_loader.dataset), running_corrects/len(dset_loader.dataset))) # 데이터개수, 정확도 출력
# return running_corrects/len(dset_loader.dataset), running_loss/len(dset_loader.dataset), inferences # 정확도, loss, inferences 반환
# 모델 학습
# def train(wandb, model, dset_loader, criterion, epoch, optimizer, logger):
# data_time = AverageMeter() # 평균, 현재값 저장
# batch_time = AverageMeter() # 평균, 현재값 저장
# lr = showLR(optimizer) # LR 변화값
# # 로거 INFO 작성
# logger.info('-' * 10)
# logger.info('Epoch {}/{}'.format(epoch, args.epochs - 1)) # epoch 작성
# logger.info('Current learning rate: {}'.format(lr)) # learning rate 작성
# model.train() # train mode
# running_loss = 0.
# running_corrects = 0.
# running_all = 0.
# end = time.time() # 현재 시각
# for batch_idx, (input, lengths, labels) in enumerate(dset_loader):
# # measure data loading time
# data_time.update(time.time() - end) # 평균, 현재값 업데이트
# # --
# # mixup augmentation 계산
# input, labels_a, labels_b, lam = mixup_data(input, labels, args.alpha)
# labels_a, labels_b = labels_a.cuda(), labels_b.cuda() # tensor 를 gpu 에 할당
# # Pytorch에서는 gradients값들을 추후에 backward를 해줄때 계속 더해주기 때문
# optimizer.zero_grad() # 항상 backpropagation을 하기전에 gradients를 zero로 만들어주고 시작을 해야 함
# # 모델 생성
# # input 텐서의 차원을 하나 더 늘리고 gpu 에 할당
# logits = model(input.unsqueeze(1).cuda(), lengths=lengths)
# loss_func = mixup_criterion(labels_a, labels_b, lam) # mixup 적용
# loss = loss_func(criterion, logits) # loss 계산
# loss.backward() # gradient 계산
# optimizer.step() # 저장된 gradient 값을 이용하여 파라미터를 업데이트
# # measure elapsed time # 경과 시간 측정
# batch_time.update(time.time() - end) # 평균, 현재값 업데이트
# end = time.time() # 현재 시각
# # -- compute running performance # 컴퓨팅 실행 성능
# _, predicted = torch.max(F.softmax(logits, dim=1).data, dim=1) # softmax 적용 후 각 원소 중 최대값 가져오기
# running_loss += loss.item()*input.size(0) # loss.item(): loss 가 갖고 있는 scalar 값
# running_corrects += lam * predicted.eq(labels_a.view_as(predicted)).sum().item() + (1 - lam) * predicted.eq(labels_b.view_as(predicted)).sum().item() # 정확도 계산
# running_all += input.size(0)
# # ------------------ wandb 로그 입력 ------------------
# wandb.log({'loss': running_loss, 'acc': running_corrects}, step=epoch)
# # -- log intermediate results # 중간 결과 기록
# if batch_idx % args.interval == 0 or (batch_idx == len(dset_loader)-1):
# # 로거 INFO 작성
# update_logger_batch( args, logger, dset_loader, batch_idx, running_loss, running_corrects, running_all, batch_time, data_time )
# return model # 모델 반환
# model 설정에 대한 json 작성
def get_model_from_json():
# json 파일이 있는지 확인, 없으면 AssertionError 메시지를 띄움
assert args.config_path.endswith('.json') and os.path.isfile(args.config_path), \
"'.json' config path does not exist. Path input: {}".format(args.config_path) # 원하는 조건의 변수값을 보증하기 위해 사용
args_loaded = load_json( args.config_path) # json 읽어오기
args.backbone_type = args_loaded['backbone_type'] # json 에서 backbone_type 가져오기
args.width_mult = args_loaded['width_mult'] # json 에서 width_mult 가져오기
args.relu_type = args_loaded['relu_type'] # json 에서 relu_type 가져오기
# TCN 옵션 설정
tcn_options = { 'num_layers': args_loaded['tcn_num_layers'],
'kernel_size': args_loaded['tcn_kernel_size'],
'dropout': args_loaded['tcn_dropout'],
'dwpw': args_loaded['tcn_dwpw'],
'width_mult': args_loaded['tcn_width_mult'],
}
# 립리딩 모델 생성
model = Lipreading( modality=args.modality,
num_classes=args.num_classes,
tcn_options=tcn_options,
backbone_type=args.backbone_type,
relu_type=args.relu_type,
width_mult=args.width_mult,
extract_feats=args.extract_feats).cuda()
calculateNorm2(model) # 모델 학습이 잘 진행되는지 확인 - 일반적으로 parameter norm(L2)은 학습이 진행될수록 커져야 함
return model # 모델 반환
# main() 함수
def main():
# wandb 연결
# wandb.init(project="Lipreading_using_TCN_running")
# wandb.config = {
# "learning_rate": args.lr,
# "epochs": args.epochs,
# "batch_size": args.batch_size
# }
# os.environ['CUDA_LAUNCH_BLOCKING']="1"
# os.environ["CUDA_VISIBLE_DEVICES"]="0" # GPU 선택 코드 추가
# -- logging
save_path = get_save_folder( args) # 저장 디렉토리
print("Model and log being saved in: {}".format(save_path)) # 저장 디렉토리 경로 출력
logger = get_logger(args, save_path) # 로거 생성 및 설정
ckpt_saver = CheckpointSaver(save_path) # 체크포인트 저장 설정
# -- get model
model = get_model_from_json()
# -- get dataset iterators
dset_loaders = get_data_loaders(args)
# -- get loss function
criterion = nn.CrossEntropyLoss()
# -- get optimizer
optimizer = get_optimizer(args, optim_policies=model.parameters())
# -- get learning rate scheduler
scheduler = CosineScheduler(args.lr, args.epochs) # 코사인 스케줄러 설정
if args.model_path:
# tar 파일이 있는지 확인, 없으면 AssertionError 메시지를 띄움
assert args.model_path.endswith('.tar') and os.path.isfile(args.model_path), \
"'.tar' model path does not exist. Path input: {}".format(args.model_path) # 원하는 조건의 변수값을 보증하기 위해 사용
# resume from checkpoint
if args.init_epoch > 0:
model, optimizer, epoch_idx, ckpt_dict = load_model(args.model_path, model, optimizer) # 모델 불러오기
args.init_epoch = epoch_idx # epoch 설정
ckpt_saver.set_best_from_ckpt(ckpt_dict) # best 체크포인트 저장
logger.info('Model and states have been successfully loaded from {}'.format( args.model_path )) # 로거 INFO 작성
# init from trained model
else:
model = load_model(args.model_path, model, allow_size_mismatch=args.allow_size_mismatch) # 모델 불러오기
logger.info('Model has been successfully loaded from {}'.format( args.model_path )) # 로거 INFO 작성
# feature extraction
if args.mouth_patch_path:
filename, embeddings = extract_feats(model)
filename = filename.split('/')[-1]
save_npz_path = os.path.join(args.mouth_embedding_out_path, filename)
# ExtractEmbedding 은 코드 수정이 필요함!
save2npz(save_npz_path, data = embeddings.cpu().detach().numpy()) # npz 파일 저장
# save2npz( args.mouth_embedding_out_path, data = extract_feats(model).cpu().detach().numpy()) # npz 파일 저장
return
# if test-time, performance on test partition and exit. Otherwise, performance on validation and continue (sanity check for reload)
if args.test:
predicthi = evaluate(model, dset_loaders['test'], criterion, is_print=False) # 모델 평가
# logging_sentence = 'Test-time performance on partition {}: Loss: {:.4f}\tAcc:{:.4f}'.format( 'test', loss_avg_test, acc_avg_test)
# logger.info(logging_sentence) # 로거 INFO 작성
return predicthi
# -- fix learning rate after loading the ckeckpoint (latency)
if args.model_path and args.init_epoch > 0:
scheduler.adjust_lr(optimizer, args.init_epoch-1) # learning rate 업데이트
epoch = args.init_epoch # epoch 초기화
while epoch < args.epochs:
model = train(wandb, model, dset_loaders['train'], criterion, epoch, optimizer, logger) # 모델 학습
acc_avg_val, loss_avg_val, inferences = evaluate(model, dset_loaders['val'], criterion) # 모델 평가
logger.info('{} Epoch:\t{:2}\tLoss val: {:.4f}\tAcc val:{:.4f}, LR: {}'.format('val', epoch, loss_avg_val, acc_avg_val, showLR(optimizer))) # 로거 INFO 작성
# -- save checkpoint # 체크포인트 상태 기록
save_dict = {
'epoch_idx': epoch + 1,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict()
}
ckpt_saver.save(save_dict, acc_avg_val) # 체크포인트 저장
scheduler.adjust_lr(optimizer, epoch) # learning rate 업데이트
epoch += 1
# -- evaluate best-performing epoch on test partition # test 데이터로 best 성능의 epoch 평가
best_fp = os.path.join(ckpt_saver.save_dir, ckpt_saver.best_fn) # best 체크포인트 경로
_ = load_model(best_fp, model) # 모델 불러오기
acc_avg_test, loss_avg_test, inferences = evaluate(model, dset_loaders['test'], criterion) # 모델 평가
logger.info('Test time performance of best epoch: {} (loss: {})'.format(acc_avg_test, loss_avg_test)) # 로거 INFO 작성
torch.cuda.empty_cache() # GPU 캐시 데이터 삭제
# 해당 모듈이 임포트된 경우가 아니라 인터프리터에서 직접 실행된 경우에만, if문 이하의 코드를 돌리라는 명령
# => main.py 실행할 경우 제일 먼저 호출되는 부분
if __name__ == '__main__': # 현재 스크립트 파일이 실행되는 상태 파악
main() # main() 함수 호출