File size: 19,841 Bytes
06d8add
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
#! /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() 함수 호출