{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"I'm sorry but , This code is written for people who are new to Kaggle in Korea kaggler community. So the commentary description is in Korean.","metadata":{}},{"cell_type":"markdown","source":"> 전체 Data Set에 대해 학습하려면 이 코드를 다운로드 후에 Local에서 진행해야합니다. Kaggle Notebook이 Hdd(5GB) 로 제한이 있기 때문에 전체 Data Set에 대해 아래 과정중 Preprocessing 과정을 진행할 수 없습니다. ","metadata":{}},{"cell_type":"markdown","source":"# 대회 소개","metadata":{"id":"QXSB10KKj4gR"}},{"cell_type":"markdown","source":"<img src=\"https://1.bp.blogspot.com/-XqqU6v6J494/XorFM6lB6zI/AAAAAAAAlTw/yYLPIwVtR4Q-KJiXycHHflY4iMCFZkV5QCK4BGAYYCw/s1600/start_doodle.PNG\"  width=\"650\" alt=\"doodle/대회메인\">","metadata":{"id":"ejx2_VRcFKXx"}},{"cell_type":"markdown","source":"\n> 이 대회는 사람이 제한시간 20초 이내에  빨리 그리는 낙서들을 Classification 하는 대회입니다. 직접 적으로 관련이 있는 Application 으로는 필기 인식 (Handwriting Recognition), 문자인식 (OCR - Optical Character Recognition)등이 있습니다.\n","metadata":{"id":"Axn5XUSUlxpj"}},{"cell_type":"markdown","source":"## Data 소개\n\n\n>  다음 url에 접속해 보면 다음과 같은 게임이 나오는데요. https://quickdraw.withgoogle.com/ 이 게임에서 발생하는 Data들이 이 Competition의 사용되었습니다. 게임 시작화면은 다음과 같습니다. 20초내에 그림을 그리라는 지시사항이 있고, 그리는 중간에 모델이 예측 후 맞추는 게임입니다.\n\n\n","metadata":{"id":"vDy7uQWoj9ts"}},{"cell_type":"markdown","source":"<img src=\"https://3.bp.blogspot.com/-Lf-9dJVZviw/XorMnfJRyXI/AAAAAAAAlUE/-xRzyYzJFqQig6_cOic0l_yUWvR_IoF4gCK4BGAYYCw/s1600/%25ED%2597%25AC%25EB%25A9%25A7.PNG\"  width=\"650\" alt=\"doodle/게임시작화면\">","metadata":{"id":"SVLBQMvYxfGo"}},{"cell_type":"markdown","source":"\n> 직접 게임에 참여해 보면, 제가 그림을 못 그리기도 하지만, 20초 시간제한에 쫓기 다 보니 어떻게 시작해야 될지 바로 안 떠올라서 더 어렵네요. 하지만 다음과 같이 현재 모델이 상당히 정확하게 맞추고 있네요. 제가 그렸지만 물고기는 사람이 알기 어려워 보이는데 모델은 잘 맞추었고, 하지만 헬멧은 모델도 못 맞추었습니다. 데이터에 대한 이해를 위해 직접 게임에 참여해 보았습니다.\n\n> Kaggle 대회를 할 때, 사람이 확인이 가능한 데이터라면 샘플 데이터 정도는 직접 확인해 보는 것을 추천 드립니다. 이미지라면 각 label별로 몇 장씩 확인하는 것, 음성이라면 각 label 별로 들어 보는 것, 동영상이라면 play해서 보는 것이 모델을 설계하고, Data Generator를 어떻게 만들지에 대한 Idea를 떠오르게 할 때가 많습니다.\n\n\n","metadata":{"id":"oe0qetWxyqjJ"}},{"cell_type":"markdown","source":"\n<img src=\"https://3.bp.blogspot.com/--yXDksuqSbs/XorMjy6LV4I/AAAAAAAAlT8/3w6oLriliD8zI-MWCXWLQ01XBr16atdFQCK4BGAYYCw/s1600/%25EC%25A2%2585%25EB%25A3%258C.PNG\"  width=\"650\" alt=\"doodle/게임종료화면\">\n","metadata":{"id":"X1xO4G7uyJ-9"}},{"cell_type":"markdown","source":"## 평가 지표\n\n> 이 대회의 평가 지표는  MAP@K(mean average precision at k) 를 쓰고 있습니다. 이 대회에서는 k가 3입니다. 주로 정보 검색 분야에서 많이 쓰는 metric으로 알고리즘이 k개 추천/예측한 값들이, 몇 번째 만에 맞춘 것인지 가중치가 반영된 점수라고 생각할 수 있습니다. \n\n> MAP@K는 test set들의 AP@K의 평균입니다. AP@K가 어떻게 계산되는지 아래 예를 통해서 살펴 보겠습니다. 정답인 'A'인 4개의 test set에 대해, 각 test set별로 prediction 값 3개씩을 제출했을 경우 아래와 같습니다. 이 대회에서는 test set 1개 row별 정답은 1개이고, 제출은 3개의 값을 하며, 중복된 값을 제출하지 않습니다.","metadata":{"id":"FXCLCmExkB7Z"}},{"cell_type":"markdown","source":"\n|정답|모델의 예측값|AP@K 점수|\n|------|:-------:|:----:|\n|A|**A**, B, C|1.0|\n|A|B, **A**, C|0.5|\n|A|B, C, **A**|0.33|\n|A|B, C, D|0.|\n\n","metadata":{"id":"pM07vRMwvfQ6"}},{"cell_type":"markdown","source":"> 최종 점수인 MAP@K는 각 test set row별 AP@K 점수들의 평균입니다. 위의 표에서는 최종 점수는 (1.0 + 0.5 + 0.33) / 4 가 최종 점수 가 됩니다. AP@K 계산에 대해 함수로 만들면 아래와 같습니다.\n\n","metadata":{"id":"sFikvLwGyZhd"}},{"cell_type":"code","source":"def apk(actual, predicted, k):\n    if actual not in predicted:  # 정답이 제출한 값에 없으면 score는 0\n        return 0.0\n    for i in range(k):\n        if actual in  predicted[:i+1]:  # 제출한 값이 K번째 만에 정답\n            return 1.0 / len(predicted[:i+1])  # score","metadata":{"id":"s0y-6M0-7mpb","execution":{"iopub.status.busy":"2023-02-22T16:09:03.927556Z","iopub.execute_input":"2023-02-22T16:09:03.927936Z","iopub.status.idle":"2023-02-22T16:09:03.93403Z","shell.execute_reply.started":"2023-02-22T16:09:03.927886Z","shell.execute_reply":"2023-02-22T16:09:03.932961Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":">정답이 'A' 인 Data가 있다고 할때, 모델이 3개의 값을 예측 할 경우 AP@K점수가 어떤 값이 나오는지 확인해 보겠습니다.","metadata":{"id":"Jxtrz0sSjIii"}},{"cell_type":"code","source":"actual ='A'\npredicted = ['A','B','C']\napk(actual,predicted, 3)","metadata":{"id":"o1QgTOYCDUp5","outputId":"215d5df2-184d-480c-c7ad-9c36c7fa5e6c","execution":{"iopub.status.busy":"2023-02-22T16:09:03.93589Z","iopub.execute_input":"2023-02-22T16:09:03.936187Z","iopub.status.idle":"2023-02-22T16:09:03.947181Z","shell.execute_reply.started":"2023-02-22T16:09:03.936143Z","shell.execute_reply":"2023-02-22T16:09:03.946237Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":">예측 한 값 중 첫번째 index의 값인 'A'가 정답과 같음으로, 가장 높은 Score인 1.0 이 됩니다.","metadata":{"id":"luyUDadwjg2a"}},{"cell_type":"code","source":"predicted = ['B','A','C']\napk(actual,predicted, 3)","metadata":{"id":"8v4gVXM6p5tl","outputId":"8429e846-6d85-46ad-f442-4c8a9eb9df8f","execution":{"iopub.status.busy":"2023-02-22T16:09:03.949138Z","iopub.execute_input":"2023-02-22T16:09:03.949448Z","iopub.status.idle":"2023-02-22T16:09:03.957763Z","shell.execute_reply.started":"2023-02-22T16:09:03.949389Z","shell.execute_reply":"2023-02-22T16:09:03.956699Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":">두번째 index의 값이 정답과 같을 경우에는 Score는 0.5 입니다.","metadata":{"id":"Nm3c2bM3j1CS"}},{"cell_type":"code","source":"predicted = ['B','C','A']\napk(actual,predicted, 3)","metadata":{"id":"SgYim9F4qACx","outputId":"6abd848c-2303-4417-d116-aa2295254d5e","execution":{"iopub.status.busy":"2023-02-22T16:09:03.960061Z","iopub.execute_input":"2023-02-22T16:09:03.960481Z","iopub.status.idle":"2023-02-22T16:09:03.96982Z","shell.execute_reply.started":"2023-02-22T16:09:03.960422Z","shell.execute_reply":"2023-02-22T16:09:03.968881Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":">세 번째 index의 값이 정답과 같을 경우에 Score는 0.33 입니다.","metadata":{"id":"dgRJ-4kskDnT"}},{"cell_type":"code","source":"predicted = ['B','C','D']\napk(actual,predicted, 3)","metadata":{"id":"ILlJJKuxqGJ5","outputId":"5b83a4d6-731c-4515-9365-7087e6025984","execution":{"iopub.status.busy":"2023-02-22T16:09:03.971011Z","iopub.execute_input":"2023-02-22T16:09:03.971407Z","iopub.status.idle":"2023-02-22T16:09:03.98056Z","shell.execute_reply.started":"2023-02-22T16:09:03.971357Z","shell.execute_reply":"2023-02-22T16:09:03.979659Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"> 예측 한 3개의 값 모두 정답이 아닐 경우에 Score는 0.0 입니다.","metadata":{"id":"XMbj_wXhkhif"}},{"cell_type":"markdown","source":"\n\n> 대회의 평가 지표에 대해 먼저 구현 및 확인하는 이유는 나중에 만들게 될 모델을 validation set으로 평가하고, training 과정 중에는 early stopping 및 learning rate schedule에 활용하며, 모델의 output을 post processing 및 ensemble 하는데 활용, 최종 대회 제출을 select 할 때에도 활용하기 때문입니다. ","metadata":{"id":"ZuJMQ0H1kv9L"}},{"cell_type":"markdown","source":"# 데이터 탐색적 분석","metadata":{"id":"kJWArYc-0MSv"}},{"cell_type":"code","source":"import os\ndata_path = \"/kaggle/input/quickdraw-doodle-recognition/\" # data set을 다운로드한 경로\nprint(os.listdir(data_path))","metadata":{"id":"4kwpomDJtU8H","outputId":"65f2847b-0f93-46f6-80a8-c3799a6f8f56","execution":{"iopub.status.busy":"2023-02-22T16:09:03.984242Z","iopub.execute_input":"2023-02-22T16:09:03.98454Z","iopub.status.idle":"2023-02-22T16:09:03.99238Z","shell.execute_reply.started":"2023-02-22T16:09:03.9845Z","shell.execute_reply":"2023-02-22T16:09:03.99141Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"> 이 대회에서 제공하는 Data 중 \"sample_submissions.csv\"을 먼저 확인하면, “key_id”가 Unique ID이고, 예측 값을 \"word\" 컬럼에 3개 공백으로 구분하여 제출해야 합니다. 한가지 주의해야 할 사항이 더 있는데, label name들 중 \"The Eiffel Tower\"처럼 복수의 단어로 구성된 경우 \"_\" 로 공백을 변환해야 한다는 것입니다. Train Data의 label Name은 아래처럼 공백으로 구분되어 있습니다.","metadata":{"id":"gri_tjfktU8M"}},{"cell_type":"code","source":"import pandas as pd\nsub_df = pd.read_csv(data_path+'sample_submission.csv')\nprint(\"test data 수:\",len(sub_df))\nsub_df.head()","metadata":{"id":"fyWGjBJStU8J","outputId":"214053fd-211d-441c-a691-e94dbb794caf","execution":{"iopub.status.busy":"2023-02-22T16:09:03.993463Z","iopub.execute_input":"2023-02-22T16:09:03.993815Z","iopub.status.idle":"2023-02-22T16:09:04.174441Z","shell.execute_reply.started":"2023-02-22T16:09:03.993747Z","shell.execute_reply":"2023-02-22T16:09:04.173678Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"> 이 대회에서 제공한 data는 raw, simplified 두 종류의 Data입니다. contrycode 는 그림을 그린 user의 국가 정보이며, key_id는 Unique Id,recognized는 기존 모델의 인식 여부, Timestamp는 그린 시간입니다.","metadata":{"id":"_Go_LSqxtU8T"}},{"cell_type":"code","source":"train_file_path = data_path + 'train_raw/'\neiffel_df = pd.read_csv(train_file_path + 'The Eiffel Tower.csv')\neiffel_df.head()","metadata":{"id":"btCbOJ8MtU8N","outputId":"9832aa27-eb1e-4104-d716-00e4abd1fd84","execution":{"iopub.status.busy":"2023-02-22T16:09:04.175675Z","iopub.execute_input":"2023-02-22T16:09:04.176219Z","iopub.status.idle":"2023-02-22T16:09:15.787888Z","shell.execute_reply.started":"2023-02-22T16:09:04.176173Z","shell.execute_reply":"2023-02-22T16:09:15.786995Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"train_simple_file_path = data_path + 'train_simplified/'\neiffel_simple_df = pd.read_csv(train_simple_file_path + 'The Eiffel Tower.csv')\neiffel_simple_df.head()","metadata":{"id":"UHua9RIUtU8R","outputId":"c1da3f0d-6202-4013-c485-d424349e0648","execution":{"iopub.status.busy":"2023-02-22T16:09:15.789445Z","iopub.execute_input":"2023-02-22T16:09:15.789785Z","iopub.status.idle":"2023-02-22T16:09:17.034233Z","shell.execute_reply.started":"2023-02-22T16:09:15.789722Z","shell.execute_reply":"2023-02-22T16:09:17.033346Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"문자열을 파이썬 list로 만들기 위해 다음 코드처럼 json.loads 함수를 사용하였습니다.","metadata":{"id":"PsKBp76H1qxS"}},{"cell_type":"code","source":"import json\nraw_images = [json.loads(draw) for draw in eiffel_df.head()['drawing'].values]\nsimple_images = [json.loads(draw) for draw in eiffel_simple_df.head()['drawing'].values]","metadata":{"id":"WgIsNQHBtU8U","execution":{"iopub.status.busy":"2023-02-22T16:09:17.035572Z","iopub.execute_input":"2023-02-22T16:09:17.035879Z","iopub.status.idle":"2023-02-22T16:09:17.045429Z","shell.execute_reply.started":"2023-02-22T16:09:17.035835Z","shell.execute_reply":"2023-02-22T16:09:17.044232Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"raw table의 drawing과 simplified의 drawing을 그려서 비교해서 확인해 보면 아래와 그림과 같은 차이가 있습니다.","metadata":{"id":"MFdtIGFl1G58"}},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nfor index in range(3):\n    plt.rc('font', size=20)\n    f, (ax1, ax2) = plt.subplots(ncols=2,nrows=1,figsize=(16,8))\n    for x,y,t in raw_images[index]:\n        ax1.plot(x, y, marker='.')\n    for x,y in simple_images[index]:\n        ax2.plot(x, y, marker='.')\n    ax1.set_title('raw drawing')\n    ax2.set_title('simplified drawing')    \n    ax1.invert_yaxis()\n    ax2.invert_yaxis()\n    ax1.legend(range(len(raw_images[index])))\n    ax2.legend(range(len(simple_images[index])))\n    plt.show('svg')  ","metadata":{"id":"aNrckDjMtU8V","outputId":"20817c76-e07f-4be8-967c-9c77b2b37c4d","execution":{"iopub.status.busy":"2023-02-22T16:09:17.047116Z","iopub.execute_input":"2023-02-22T16:09:17.047763Z","iopub.status.idle":"2023-02-22T16:09:18.329964Z","shell.execute_reply.started":"2023-02-22T16:09:17.047684Z","shell.execute_reply":"2023-02-22T16:09:18.329034Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"> 위 에펠탑 그림에서 color는 획 순입니다. raw 좌표는 포인트들이 많은 반면, simplified의 포인트는 Raw point에서 같은 방향으로 이동한 부분들을 생략하여 Point 수가 적습니다. 그 외에도 raw의 X, Y 축 Range를 보면 일정하지 않습니다. 그림을 그린 device 창 크기에 따라 raw 좌표가 다르기 때문으로 예상됩니다. simplified drawing은 256 by 256으로 전 처리 되어있습니다. 또한 raw data에는 시간정보가 있으며, 공식적으로 명시하지는 않았지만, m-sec 단위로 추측됩니다.\n","metadata":{"id":"2QHyb9bytU8X"}},{"cell_type":"code","source":"print(\"======== 첫번째 raw drawing의 첫 획 Data 중 5개 Point 정보 =========\")\nprint(\"x좌표: \", json.loads(eiffel_df['drawing'][0])[0][0][:5])\nprint(\"y좌표: \", json.loads(eiffel_df['drawing'][0])[0][1][:5])\nprint(\"msec: \", json.loads(eiffel_df['drawing'][0])[0][2][:5])\n\nprint(\"======== 첫번째 Simplified drawing의 첫 획 Data 중 5개 Point 정보 =========\")\nprint(\"x좌표: \", json.loads(eiffel_simple_df['drawing'][0])[0][0][:5])\nprint(\"y좌표: \", json.loads(eiffel_simple_df['drawing'][0])[0][1][:5])","metadata":{"id":"mfuF3IO7tU8X","outputId":"781833ac-80bf-4ba7-ee08-b8e7f5fdd4fb","execution":{"iopub.status.busy":"2023-02-22T16:09:18.331184Z","iopub.execute_input":"2023-02-22T16:09:18.331478Z","iopub.status.idle":"2023-02-22T16:09:18.361206Z","shell.execute_reply.started":"2023-02-22T16:09:18.331433Z","shell.execute_reply":"2023-02-22T16:09:18.360262Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"\n> Data Handling 및 쉬운 모델 작성을 위해서는 simplified data를 사용해도 무방하겠지만, 여기서는 메달권 점수를 얻는 것이 목표임으로, 정보가 많은 raw data를 사용하겠습니다.","metadata":{"id":"KDzPGML8I1H7"}},{"cell_type":"markdown","source":"# Data Preprocessing","metadata":{"id":"2u1pRIZRtU8Z"}},{"cell_type":"markdown","source":"> training을 위한 raw파일들의 용량을 확인해 보면 총 204GB로 모두 Loading 하는 것은 Data 컴퓨터 리소스 상 어렵습니다. 때문에 training을 위한 파일을 분할하겠습니다.","metadata":{"id":"i1fG9hj5tU8Z"}},{"cell_type":"code","source":"train_csvs= os.listdir(train_file_path)\nprint(\"train_raw 폴더 내 파일 수:\", len(train_csvs))\nprint(train_csvs[:5])\n\nfile_size = 0\nlabel_names = []\nfor csv_file in train_csvs:\n    file_size += os.path.getsize(train_file_path + csv_file) # data file들의 용량을 계산\n    label_names.append(csv_file.replace('.csv','')) \nprint(\"파일 크기 : \", file_size//(1024*1024*1024) ,\"GB\")","metadata":{"id":"eR7XEDGytU8a","outputId":"0979065c-bfb4-49c7-9528-fa3b86a7267a","execution":{"iopub.status.busy":"2023-02-22T16:09:18.362791Z","iopub.execute_input":"2023-02-22T16:09:18.363232Z","iopub.status.idle":"2023-02-22T16:09:18.447619Z","shell.execute_reply.started":"2023-02-22T16:09:18.36315Z","shell.execute_reply":"2023-02-22T16:09:18.446689Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"> Kaggle 대회를 하다 보면 간혹 이렇게 data의 양이 큰 대회들이 있습니다. 이럴 경우에는 개인 PC에서 \n진행하려면 하드웨어의 성능이 있어야 수훨합니다. 저의 경우에는 SSD의 용량이 부족해서 다음과 그림과 같이 학습에서 \n사용할 data 파일들을 압축해서(압축 후 100GB 수준) SSD에 넣고  기존 파일들을 삭제하였습니다. 이렇게 변경하는 이유는 Train 과정중에는 label 별로 data를 섞은 후 Model의 input으로 주어야 하지만, 204G 에 해당하는 파일을 모두 메모리에 로딩하고, Shuffle해서 Input Data로 쓸 수 없기 때문입니다. Image로 미리 변환하지 않은 이유는 5000만개 image를 저장할 hdd용량이 부족합니다. numpy array나 image로 파일을 저장해놓으면 7TB 정도가 필요할 것으로 예상됩니다. 때문에 저는 다음과 같이 Training 전에 1회만 Data 준비를 진행하였습니다. 이 작업은 1회만 수행하도록 하였고 변환작업은 대략 4시간 정도 소요됩니다.","metadata":{"id":"WNgTFWzxtU8d"}},{"cell_type":"markdown","source":"<img src=\"https://1.bp.blogspot.com/-VuXMw8jhwIM/XqJ9tEG3tUI/AAAAAAAAlcE/wIOfyFQ8yN8bBAyWR2JzgzQ8MfwWzXvjwCLcBGAsYHQ/s1600/file_convert.PNG\"  width=\"650\" alt=\"doodle/file_convert\">\n","metadata":{"id":"SmGnbDnJtU8e"}},{"cell_type":"code","source":"# kaggle hdd 용량이 작음(5GB)으로 local에서 가능\nfrom tqdm import tqdm\ndivide_shuffles = 100 # data를 분할 할 file 수\nraw_shuffle_data_path = data_path + 'shuffle_raw_gzs/'\ntry:\n    os.mkdir(raw_shuffle_data_path)\n    for y, csv_file in enumerate(tqdm(train_csvs)):\n        df = pd.read_csv(train_file_path+csv_file, usecols= ['drawing','key_id'])\n        df['y'] = y # label\n        df['cv'] = (df.key_id//10000) % divide_shuffles # keyid로 data 나누기.\n        for k in range(divide_shuffles):\n            filename = raw_shuffle_data_path +f'train_k{k}.csv'\n            chunk = df[df.cv == k] # 0~99 까지 cv에 번호로 select\n            chunk = chunk.drop(['key_id','cv'], axis=1)\n            if y == 0:\n                chunk.to_csv(filename, index=False) # 처음이면 파일을 만들고\n            else:\n                chunk.to_csv(filename, mode='a', header=False, index=False) # add   \n    for k in tqdm(range(divide_shuffles)):\n        filename = raw_shuffle_data_path +f'train_k{k}.csv'\n        df = pd.read_csv(filename) # 아직 까지는 label 순서대로 data가 만들어 져 있음.\n        # label 별 파일의 내용을 shuffle 하기 위해 랜덤 값 추가\n        df['rnd'] = np.random.rand(len(df)) \n        # 추가된 랜덤값으로 정렬하여 순서를 shuffle함\n        df = df.sort_values(by='rnd').drop('rnd', axis=1) \n        # ssd 용량이 부족하지 않다면 compression은 빼는게 빠름\n        df.to_csv(filename.replace('.csv','.gz'),compression='gzip', index=False) \n        os.remove(filename)\nexcept:\n    print(\"shuffled train data 준비는 한번만 실행합니다.\")\n    pass","metadata":{"id":"c2BRHHHxtU8e","outputId":"75985cf3-65d0-4759-d25f-52ba9ea9c805","execution":{"iopub.status.busy":"2023-02-22T16:09:18.450424Z","iopub.execute_input":"2023-02-22T16:09:18.450909Z","iopub.status.idle":"2023-02-22T16:09:18.465727Z","shell.execute_reply.started":"2023-02-22T16:09:18.450831Z","shell.execute_reply":"2023-02-22T16:09:18.464528Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{"id":"nNRTjfc2tU8h","trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"test_raw_df = pd.read_csv(data_path+\"test_raw.csv\")\ntest_raw_df.head()\nprint(test_raw_df.shape)","metadata":{"id":"Ek5U6899tU8j","outputId":"810468a1-cf4e-4c6a-9234-3d106b9124f7","execution":{"iopub.status.busy":"2023-02-22T16:09:19.504015Z","iopub.execute_input":"2023-02-22T16:09:19.504357Z","iopub.status.idle":"2023-02-22T16:09:32.442506Z","shell.execute_reply.started":"2023-02-22T16:09:19.504319Z","shell.execute_reply":"2023-02-22T16:09:32.441512Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"test_raw_df.shape[0]%len(label_names) ","metadata":{"id":"gWG5EV8-tU8k","outputId":"0b482ca6-74c4-49ee-8772-7b9d574aa132","execution":{"iopub.status.busy":"2023-02-22T16:09:32.444237Z","iopub.execute_input":"2023-02-22T16:09:32.444647Z","iopub.status.idle":"2023-02-22T16:09:32.449915Z","shell.execute_reply.started":"2023-02-22T16:09:32.444495Z","shell.execute_reply":"2023-02-22T16:09:32.448957Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Data Generator ","metadata":{"id":"0048EcjDkIO3"}},{"cell_type":"markdown","source":"> 이번 대회의 데이터를 단순 위처럼 좌표 그림만 그려서 CNN 모델로 학습할 경우에도 어느정도(MAP@3 0.90이상) 점수를 얻을 수 있습니다. 하지만 이 코드에서는 딥러닝 모델에 더 많은 정보를 input으로 만들어서 더 높은 점수를 얻도록 하겠습니다. ","metadata":{"id":"ARrmDwC-NR7g"}},{"cell_type":"markdown","source":"## Draw Image with Extra Features","metadata":{"id":"rH6qN4G8tU8m"}},{"cell_type":"markdown","source":"> 이전에 살펴 본것처럼 raw stoke의 정보는 X좌표, Y좌표, msec(시간) 의 list로 이루어져 있습니다. 여기서 얻을 수 있는 추가 Feature로 제가 생각한 것은 다음과 같습니다.\n   \n>   \n    1. 획의 순서\n    2. 획의 포인트별 이동 속도\n    3. 획의 포인트별 이동 속도의 변화량 \n    4. 획의 방향, 획과 획 사이 공백 시간, 전체 그림을 그리는데 걸린 시간, 전체 그림의 시작, 종료 Point \n    \n> 예를 들어 Smile Face Data 한 개를 그려 보겠습니다. 총 3획, Point 수는 14Point, 각 획 별 Point 수는 5,5,3개인 Data입니다. 제공된 Data는 아니고 Feature 추가 방법에 대해 이해를 돕기 위해 단순 화 하여 제가 만든 Data입니다. \n\n> <img src=\"https://1.bp.blogspot.com/-zcfi12zr6Jo/Xp_82LeXUnI/AAAAAAAAlaU/mL4TC4QNxJ8Pzzf_4iFcGcFM0zVuzb2sQCLcBGAsYHQ/s1600/point.PNG\"  width=\"400\" alt=\"doodle/smile_face_sample_point\">\n\n> 만약 완성 된 그림으로만 학습한다고 하면 다음과 같은 이미지가 될 것입니다. \n\n> <img src=\"https://1.bp.blogspot.com/-y6Tl147OzXw/Xp_81wDva1I/AAAAAAAAlaM/u1-5ePtcCWsIGp_twN5WrRKYjn42fTJhwCLcBGAsYHQ/s1600/org.PNG\"  width=\"400\" alt=\"doodle/smile_face_sample_org\">\n\n> 1. 획의 순서는 0번(Red) channel에 값으로 첫획을 밝게 하고 점점 어두워지도록 표시를 하였습니다. 이와 같은 방식으로 실행해보면 다음과 같은 그림이 그려집니다.\n\n> <img src=\"https://1.bp.blogspot.com/-13PvU7mRCw8/Xp_815WhjuI/AAAAAAAAlaQ/9TcRlRbuPlQTUW_7_8hCdWhIZoGUfgG4wCLcBGAsYHQ/s1600/order.PNG\"  width=\"400\" alt=\"doodle/smile_face_sample_order\">\n\n> 2. 획의 포인트별 이동 속도는 포인트와 포인트 사이의 euclidiean pixel 거리에 걸린 시간을 나누어 주었습니다. 즉 이동 속도가 빠를 수록 밝은 값을 갖습니다. Channel들을 합치면 color 영상이 나오나 색감 구분이 잘 되지 않아서 다음 그림처럼 약간 윗쪽에 1번 channel을(Green) 표시하였습니다.\n\n> <img src=\"https://1.bp.blogspot.com/-RK6VhVwNfTc/Xp_81FnbjqI/AAAAAAAAlaE/yqQ9FkprL_AAnCeaFjh6LIPbKHRfO0AFQCLcBGAsYHQ/s1600/add_v.PNG\"  width=\"400\" alt=\"doodle/smile_face_sample_v\">\n\n> 3. 획의 포인트 별 이동 속도의 변화량은 이전 포인트에서 현재 포인트까지 이동을, 관성 그대로 다음 포인트까지 이동했을 때 점에서, 실제 다음 포인트까지의 거리에 걸린 시간을 나누어, 2번 channel에(Blue) 표시하였습니다. 그림을 그려보면 다음과 같습니다. 점선이 예상 point이고, 예상point로부터 실제 point까지의 거리를 밝기로 나타낸 것입니다. 쉬운 식별을 위해 이번에는 약간 아래쪽에 2번 channel을(Blue) 표시하였습니다. 이동 방향과 거리 변화가 급변할 수록 밝은 값을 갖도록 하였습니다.\n\n> <img src=\"https://1.bp.blogspot.com/-No75NjGeHOE/Xp_81KJxN6I/AAAAAAAAlaA/Y0pim-kUKFEYh-CaV1c-RH6WPeCRGQDvQCLcBGAsYHQ/s1600/add_a.PNG\"  width=\"400\" alt=\"doodle/add_a\">\n\n> 4. 획의 방향은 획의 시작 점 2번 channel에(Blue) circle을 그려서 표시를 하게 했습니다. 또한 밝기 정보로 획과 획 사이의 공백 시간을 나타내게 하였는데, 이전 획을 그리고 다음 획을 시작하는데 걸린 시간이 길수록 더 밝게 표시하였습니다. 그리고 전체 그림을 그리는데 걸린 시간은 마지막 Point에 1번 Channel의(Green) 밝기 값으로 표시하겠습니다.\n\n> <img src=\"https://1.bp.blogspot.com/-5OXXJOeNKMg/Xp_81etBKbI/AAAAAAAAlaI/Cq3-WkBYu-QYOUUSNQNzGdU0jk3dXha2wCLcBGAsYHQ/s1600/add_interval.PNG\"  width=\"400\" alt=\"doodle/interval\">\n\n   ","metadata":{"id":"y356PrajtU8n"}},{"cell_type":"code","source":"import cv2\nimport numpy as np\ndef draw_raw_cv2(raw_strokes,size=128,lw=6,last_drop_r=0.0,second_strokes = None):  \n    ofs = lw*2 # 완성된 이미지에 테두리 공백 \n    limit_ett = 20*1000 # 최대 시간 20초\n    npstrokes = [] \n    mminfo={\"xmin\":float('inf'),\"ymin\":float('inf')\n        , \"xmax\":float('-inf'),\"ymax\":float('-inf')} \n    \n    # strokes drop augmentation\n    drop_num = int(np.random.random()*last_drop_r *len(raw_strokes))\n    if drop_num>0:\n        raw_strokes = raw_strokes[:-drop_num]\n    \n    # mixup augmentation\n    if second_strokes is not None:\n        first_ett = raw_strokes[-1][-1][-1]\n        end_fist_st_len = len(raw_strokes)\n        raw_strokes.extend(second_strokes)\n        \n    for t, stroke in enumerate(raw_strokes):\n        npstroke = np.array(stroke)\n        #print(npstroke.shape)\n        npstrokes.append(npstroke)        \n        mminfo[\"xmin\"] = min(mminfo[\"xmin\"], min(npstroke[0]))\n        mminfo[\"xmax\"] = max(mminfo[\"xmax\"], max(npstroke[0]))\n        mminfo[\"ymin\"] = min(mminfo[\"ymin\"], min(npstroke[1]))\n        mminfo[\"ymax\"] = max(mminfo[\"ymax\"], max(npstroke[1]))\n        \n    ett=npstrokes[-1][-1][-1] # 얼마나 빨리 완료하는가 20초 이하  \n    nimg = np.zeros((size,size,3),dtype=float)\n    # print(mminfo) # min 좌표에 음수가 있는 경우도 있음.   \n    org_width = mminfo[\"xmax\"] - mminfo[\"xmin\"] \n    org_height = mminfo[\"ymax\"] - mminfo[\"ymin\"]\n    ratio = max(org_width,org_height) / (size-ofs*2)\n    if ratio == 0 :\n        print('ratio 0 case ? null data ? log for debugging',mminfo)\n        return nimg\n    pre_st_t = 0 \n    for t, stroke in enumerate(npstrokes):\n        stroke[0] = (stroke[0] - mminfo[\"xmin\"])/ratio + ofs\n        stroke[1] = (stroke[1] - mminfo[\"ymin\"])/ratio + ofs\n        inertia_x = 0\n        inertia_y = 0\n        if second_strokes is not None and t == end_fist_st_len:\n            pre_st_t = 0\n        for i in range(len(stroke[0]) - 1): # 각 stroke의 Point loop, 마지막 좌표 전까지\n            color = min((1.0 - 0.95*float(t)/len(npstrokes)),1.0) # 획 순에 대한 color\n            sx = int(stroke[0][i])\n            sy = int(stroke[1][i])\n            st = stroke[2][i]\n            ex = int(stroke[0][i + 1])\n            ey = int(stroke[1][i + 1])\n            et = stroke[2][i+1]\n            ## like 속력, 비 정상 data 가 있음. et-st 가 음수인경우 et-st가 0인 경우\n            color_v = min((((sx-ex)**2+(sy-ey)**2)**0.5 / (abs(et-st)+1) *5), 1.0) \n            if i==0:\n                color_a = 0\n            else: ## 획의 변화량, like 가속력, 첫점은 255   \n                color_a = min((((inertia_x-ex)**2 +\n                 (inertia_y-ey)**2)**0.5 / (abs(et-st)+1) *5), 1.0)          \n            nimg = cv2.line(nimg, (sx, sy), (ex, ey), (color,color_v,color_a), lw)\n            # print(color_v,color_a)\n            if i==0:\n                color_inter = min((float(st-pre_st_t)*10/limit_ett),1.0)\n                if t == 0 or (second_strokes is not None \n                              and t == end_fist_st_len):\n                    color_inter = 1.0 # 첫 stroke의 첫번째 점 표시\n                # interval time\n                nimg = cv2.circle(nimg, (sx, sy), lw\n                                  , (0.0,0.0,color_inter), -1)\n            # 마지막 획에 마지막 점    \n            if i==len(stroke[0])-2 and t == len(raw_strokes) -1: \n                color_end = (float(ett)/(limit_ett)) # end time\n                nimg = cv2.circle(nimg, (ex, ey), lw\n                                  , (0.0,color_end,0.0), -1) \n            # mix up augmentation 마지막 획에 마지막 점\n            if second_strokes is not None \\\n            and i==len(stroke[0])-2 and t == end_fist_st_len -1: \n                color_end = (float(first_ett)/(limit_ett)) # end time\n                nimg = cv2.circle(nimg, (ex, ey), lw, (0.0,color_end,0.0), -1) \n                \n            inertia_x = ex + (ex-sx)\n            inertia_y = ey + (ey-sy)\n            pre_st_t=et         \n    return nimg","metadata":{"id":"9Lm3028htU8n","execution":{"iopub.status.busy":"2023-02-22T16:09:32.451428Z","iopub.execute_input":"2023-02-22T16:09:32.451719Z","iopub.status.idle":"2023-02-22T16:09:32.7326Z","shell.execute_reply.started":"2023-02-22T16:09:32.451677Z","shell.execute_reply":"2023-02-22T16:09:32.731457Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"> 이번엔 실제 Train Data를 위에서 정의한 방식대로 위 함수를 이용하여 그려 보겠습니다. Preprocessing 단계에서 생성한 파일 중 하나를 열어서 그려 보았습니다. Label은 \"mug\" 입니다. 사람의 눈으로 미세한 밝기 값 까지는 구분할 수 없지만, 하나의 획이 어느 점에서 시작하고 어디에서 끝나며, 어떤 순서로 그려졌으며, 어떤 획을 빨리 그리고, 어떤 획을 천천히 그렸는지 대략 판단할 수 있습니다.단할 수 있습니다.\n\n\n> <img src=\"https://1.bp.blogspot.com/-wJSwSqOYEvo/XqAcTtJa5DI/AAAAAAAAlas/3UKDXfAaHkkAVm5aEPjplBrBD0PIZksmQCLcBGAsYHQ/s1600/mug_org.PNG\"  width=\"300\" alt=\"doodle/interval\">\n\n   \n","metadata":{"id":"Ya4wT9fztU8p"}},{"cell_type":"code","source":"# 이후 열 부터는 kaggle 노트북의 hdd 용량이 작음으로 local에서 작업해야함\nraw_shuffle_data_path","metadata":{"execution":{"iopub.status.busy":"2023-02-22T16:11:26.509484Z","iopub.execute_input":"2023-02-22T16:11:26.509927Z","iopub.status.idle":"2023-02-22T16:11:26.516477Z","shell.execute_reply.started":"2023-02-22T16:11:26.509864Z","shell.execute_reply":"2023-02-22T16:11:26.515037Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"train_file0_df = pd.read_csv(raw_shuffle_data_path+'train_k0.csv')\ndraw_test = draw_raw_cv2(json.loads(train_file0_df.loc[0].drawing),  size=128, lw=2)\nfig,ax = plt.subplots(figsize=(5,5))\nax.set_title(label_names[train_file0_df.loc[0].y])\nax.imshow(draw_test)\nplt.show()","metadata":{"id":"PCsGOwG9tU8t","outputId":"cbbff1e3-4e85-4b47-d0ad-0e5047f06935","trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Data Augmentation","metadata":{"id":"KnjNUvEttU8v"}},{"cell_type":"markdown","source":"> Image Classification, Segmentation 대회는 물론, 여러 분야의 대회에서 Augmentation은 점수 향상에 큰 영향을 미칩니다. 때문에 어떤 조합의 Augmentation을 사용하는 것이 좋을지 탐색하고, 다른 사람이 생각하지 않는 창의적인 Augmentation을 찾아 추가하는데 상당한 노력을 하게 됩니다. 하지만 이 대회는 Data 수가 5000만개 나 되어서, 작은 모델을 선택해도 1epoch을 학습하는데 하루 이상이 소요될 것으로 예상되고, 많은 수의 epoch을 반복하지 않을 것이기 때문에 Augmentation은 학습 시간(cpu부하)이 늘어나지 않는 방법만 최소로 적용하겠습니다. 제가 위 데이터를 보면서 생각한 Augmentation 방법은 다음과 같습니다.\n>> \n    1. 획 중 뒷 순서의 획들을 랜덤하게 삭제 \n    2. 두 종류의 그림을 동시에 그리기\n\n> 획 중 뒤쪽 순서의 획들을 random하게 삭제한 이유는 시간이 부족하면 뒤쪽 획들을 그리지 못한 채 게임이 종료될 것이고, 그 부분 없이도 무엇을 그렸는지 구분할 수 있는 모델이 더 잘 학습된 모델이라고 생각했습니다. 그리고 두 종류의 그림을 동시에 그린 이유는 두가지가 섞여서 있을 때는 두가지 Label에 대해 예측 확률이 비슷하게 나오는 모델이 더 잘 학습된 모델이기 때문입니다. \n\n> 전에 살펴본 \"mug\" Label Data에 대해 Augmentaion을 적용 해보겠습니다. 다음의 이미지 중, 가운데 이미지는 획중에 마지막 두개가 없어진 영상입니다. 두개가 없어져도 \"mug\"라고 인식하는데는 영향이 없어 보입니다. 또한 마지막 획이 사라지면서 object의 크기가 조정 되는 Augmentation 효과도 있습니다. 이 Augmetation은 CPU 부하를 전혀 늘리지 않습니다. 오른쪽 이미지는 \"spider\" Label Data가 섞여 있는 이미지 입니다. Spider가 섞일때에도, 개별 그림들의 크기가 조정 되는 효과가 있으며, 두개 이미지를 동시에 그리다 보니 배치가 달라져서 Random Crop 과 같은 Augmentaion 효과도 있습니다. 몇개의 획을 더 그리다 보니 CPU 부하가 있지만, Rotate, Shear, Blur 등 보통의 Image Augmentation에 비해 부하량은 매우 적습니다.\n\n> <img src=\"https://1.bp.blogspot.com/-C01OvZyvrHQ/XqAcTm9gtII/AAAAAAAAlaw/l9jjs9tTL2kM8OSBMeNaTWbqvElC-gkCQCLcBGAsYHQ/s1600/mug_aug.PNG\"  width=\"650\" alt=\"doodle/interval\">\n\n","metadata":{"id":"nV_HGqYYtU8v"}},{"cell_type":"code","source":"","metadata":{"id":"nLXYe2BatU8v"},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"#  check last drop augmentation\ndraw_test_drop=draw_raw_cv2(json.loads(train_file0_df.loc[0].drawing),size=128\n                              ,lw=2,last_drop_r=0.3)\ndraw_test_drop_mix=draw_raw_cv2(json.loads(train_file0_df.loc[0].drawing)\n                            ,size=128,lw=2,last_drop_r=0.3 \n                ,second_strokes=json.loads(train_file0_df.loc[100].drawing))\nfig,ax = plt.subplots(ncols=3,figsize=(15,5))\nax[0].set_title(label_names[train_file0_df.loc[0].y] + ' ▶ orginal')\nax[0].imshow(draw_test)\nax[1].set_title(label_names[train_file0_df.loc[0].y] + ' ▶ drop_aug')\nax[1].imshow(draw_test_drop)\nax[2].set_title(label_names[train_file0_df.loc[0].y] + ' ▶ mix ' + \n                label_names[train_file0_df.loc[100].y])\nax[2].imshow(draw_test_drop_mix)\nplt.show()","metadata":{"id":"t1e9DKiYtU8x","outputId":"264cd3e6-d5c9-4c4f-d85d-557f20023ba8","trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Generator","metadata":{"id":"qmiumAl5u3Ie"}},{"cell_type":"markdown","source":"> 이 대회의 Generator의 경우에는 train파일 용량이 너무 크기(약 200GB) 때문에 메모리에 한번에 올려 놓고 모델에 input data를 공급할 수가 없습니다. 또한 전체 row개수 또한 너무 많아서(약 5000만개) 이미지나, numpy array로 미리 변환(약 7TB) 해 놓을 수도 없습니다. 때문에 이전 전처리 과정에서 만들어 놓은 100개의 파일들을 Training 과정 중에 하나씩 읽고, 그 파일들의 row들을 읽어서 모델에 공급하도록 Generator를 만들겠습니다. 여기서 만들 Generator의 전체 flow를 보면 다음 그림과 같습니다. 보통 training set 전체를 1번 학습하는 것을 1Epoch 라고 부릅니다. 여기서는 1Epoch의 약 100분의 1인 1개 파일 전체를 학습하는것을 Virtual 1Epoch라고 부르겠습니다. 이 대회의 Data는 전체를 학습하는데 하루가 넘게 걸리기 때문에, 많아야 3~5Epoch 학습할 수 있을 것으로 예상됩니다. 때문에 Training 과정중에 보통 진행하는 Validation Data 평가, Validation 평가 점수 기준으로 수행 할 Best Model 저장 및 Learning Rate 조절은 Virtual Epoch 기준으로 수행 하겠습니다. 그림에서 현재 Virtual Epoch가 끝나면, 다음 파일은 \"Train1.gz\"를 로딩하면서 기존 메모리는 Release되게 됩니다. 즉 주황색 화살표 Line이 1Virtual Epoch마다 한칸씩 밑으로 내려 가면서 동일 작업을 수행하게 할 것입니다. \n\n> <img src=\"https://1.bp.blogspot.com/-bKkaGVP0W0I/XqKA2mOP2lI/AAAAAAAAlcY/VfGrS33i09AAXpGU_6fJu7--tJI4KBXpgCLcBGAsYHQ/s1600/generator.PNG\"  width=\"650\" alt=\"doodle/generator\">\n\n\n> 그리고 이전 Image Drawing 과정에서 Mix-Up Augmentation의 경우는 Label에 절반 정도의 가중치로 섞어서 주게 끔 할 것입니다. 또한 Augmentation된 Image를 보면 알아보기 어렵기 때문에 Label Smoothing을 할 것입니다. Label Smoothing은 보통 Data의 Noise가 (Label이 잘못 붙어있는) 많은 경우, Label을 \"[0, 0, ..., 1, 0, 0]\"으로 주지 않고 \"[0.01, 0.01, ..., 0.9, 0.01]\"처럼 Label에 대한 확신을 줄여서 학습하는 방법입니다.","metadata":{"id":"kP248CwlvFha"}},{"cell_type":"code","source":"import tensorflow as tf\nimport numpy as np\nimport json\nfrom tensorflow.keras.utils import to_categorical      \nclass DoodelGenerator(tf.keras.utils.Sequence):\n    def __init__(self,df_files,input_shape,batchsize,label_num=340\n                 ,lw=3,state='Train',last_drop_r=0.0,mixup_r = 0.0):\n        self.df_files = df_files\n        self.file_sel = 0 # 파일 list 중 현재 fit하는데 사용할 파일 index\n        self.batchsize = batchsize\n        self.input_shape = input_shape\n        self.label_num = label_num\n        self.lw = lw\n        self.state = state\n        self.last_drop_r = last_drop_r\n        self.mixup_r = mixup_r\n        self.on_epoch_end()\n        self.len = -(-len(self.df)//self.batchsize) \n\n    def __len__(self):\n        return self.len\n\n    def __getitem__(self, index):\n        # batch size 만큼 index를 뽑음\n        batch_idx = self.idx[index*self.batchsize:(index+1)*self.batchsize] \n        h,w,ch = self.input_shape\n        X = np.zeros((len(batch_idx), h,w,ch)) #batch\n        y = np.zeros((len(batch_idx), self.label_num))\n        df = self.df.loc[batch_idx]\n        mixup_num = int(self.batchsize*self.mixup_r)\n        mixup_df = self.df.loc[np.random.randint(0,len(self.df),size=mixup_num)]\n        mixup_strokes=[]\n        mixup_labels=[]\n        for raw_strokes, label in mixup_df.values:\n            mixup_strokes.append(json.loads(raw_strokes))\n            mixup_labels.append(label)\n            \n        for i in range(self.batchsize):\n            raw_strokes = json.loads(df.drawing.values[i])\n            if i < len(mixup_strokes):\n                X[i, :, :, ] = draw_raw_cv2(raw_strokes, size=h, lw=self.lw\n                ,last_drop_r = self.last_drop_r,second_strokes=mixup_strokes[i])\n                if self.state != 'Test':\n                    ysm_mix = self.smooth_labels(to_categorical(mixup_labels[i]\n                                                , num_classes=self.label_num))\n                    ysm_org = self.smooth_labels(to_categorical(df.y.values[i]\n                                                , num_classes=self.label_num))\n                    y[i, :] = (ysm_mix*0.5) + (ysm_org*0.5)\n            else:\n                X[i, :, :, ] = draw_raw_cv2(raw_strokes, size=h, lw=self.lw\n                                        , last_drop_r = self.last_drop_r)\n            \n                if self.state != 'Test':\n                    y[i, :] = to_categorical(df.y.values[i]\n                                             ,num_classes=self.label_num)\n            \n        if self.state != 'Test':\n            return X,y\n        else:\n            return X\n    \n    def get_cur_df(self): # 현재 로딩되어 있는 파일을 반환하는 함수, holdout set 평가시 사용\n        return self.df \n    \n    def smooth_labels(self,labels, factor=0.1): # mix up augmentation 사용시 사용\n        labels *= (1 - factor)\n        labels += (factor / labels.shape[0])\n        return labels\n    \n    def on_epoch_end(self):\n        self.df = pd.read_csv(self.df_files[self.file_sel])\n        print('current step file : ', self.df_files[self.file_sel]\n              ,'state:', self.state, 'df_len:', self.df.shape[0])\n        self.idx = np.tile(np.arange(len(self.df)),2) # train file size가 flexible함으로 idx 배열을 연장\n        if self.state == 'Train':\n            np.random.shuffle(self.idx)        \n        self.file_sel = (self.file_sel+1)%len(self.df_files) # next csv file roll\n    ","metadata":{"id":"3GBqM-eQtU80","trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":">>> 참고. 이 코드는 책 집필시 최신 버전인 tensorflow 2.1 기준으로 작성했습니다. tensorflow는 1.x버전에서 2.x 버전으로 바뀌면서 상당히 많은 부분이 바뀌었습니다. 때문에 사용하다보면 여러 bug가 많이 발견됩니다. 하지만 워낙 커뮤니티가 크다 보니 빠르게 안정화 될 것으로 생각합니다.\n\n> 이전 과정에서 만든 Generator의 on_epoch_end 함수에 1 Virtual Epoch 마다, 새로운 파일들을 Loading하도록 만들었지만, model.fit 을 수행하면 on_epoch_end가 호출되지 않습니다. tensorflow ver2.1에 bug가 있는 상태이기 때문입니다. 그래서 다음과 같은 Custom Callback을 추가하겠습니다. 이 Callback이 하는 역할은 단순 on_epoch_end를 호출해주는 기능입니다.","metadata":{"id":"0CxDsxN7TL3Q"}},{"cell_type":"code","source":"# tensorflow ver 2.1 의 bug, model.fit 에서 tf.keras.utils.Sequnce의 on_epoch_end 를 호출하지 않음\n# 별도의 callback에서 호출하도록 추가, 추후 버그 수정버전으로 release 되면 삭제해도 되는 부분\nclass OnEpochEnd(tf.keras.callbacks.Callback):\n    def __init__(self, callback): # train generator의 on_epoch_end 콜백 인자로 받음\n        self.callback = callback\n        \n    def on_epoch_end(self, epoch, logs=None): # 단순 callback을 호출 해줌\n        self.callback()","metadata":{"id":"Nkl3zIhT7i5o","trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{"id":"Al32W-r4jcyj","trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"> 이전 과정에서 Generator를 만들었으니, Data 생성에 결함이 없는지 Check 해보겠습니다. 대부분의 딥러닝 대회에서는 Generator를 만들게 되면, Data 생성이 제대로 되는지 항상 Check를 해보아야 합니다. 여러 오류들을 범하게 되기 때문에 꼭 Image 및 Label이 원하는 대로 나오는지 확인이 필요합니다. 다음과 같은 오류들은 코드 오류는 아니어서 바로 확인은 안되고 Training 후에 점수가 낮게 나오고 나서 확인이 되기 때문에, 미리 Check를 하면 상당한 시간을 절약할 수 있습니다. 제가 주로 Check 하는 Point는 다음과 같습니다.\n\n>> \n    1. Image와 Label이 잘못 Matching된 경우\n    2. Train Data가 Shuffle이 안 된 경우\n    3. Augmentation 결과가 실제 Data에서는 절대 나오지 않을 법한 Image인 경우\n\n\n> 다음과 같은 코드로 generator로 부터 25개 1batch sample을 받은 후 Display 해 보았습니다. \"dishwasher\" 이미지는 \"mug\" Label을 가진 Data에 mix-up augmentation 이 적용되어 나온 이미지고, Label은 argmax로 표시하여 dishwasher가 되었지만, raw data는 mug, dishwasher가 각각 0.45 씩 이고 나머지 Label들이 0.1 을 나누어 갖는 값으로 Smoothing 되어 의도대로 생성 되었습니다. 우하단의 \"elbow\" Label을 가지는 이미지는 전혀 \"elbow\"처럼 보이지 않습니다. 혹시나 generator에 오류가 있는지, 파일을 직접 열어 data 자체가 noisy함을 (오류들이 간혹 있음을) 확인하였습니다. \n\n> <img src=\"https://1.bp.blogspot.com/-fOCFEO5__xc/XqBbne7qa7I/AAAAAAAAlbI/9jzFoNi16H8XbMehY0cnj9FwTaurCW9TQCLcBGAsYHQ/s1600/gen_check.PNG\"  width=\"650\" alt=\"doodle/generator\">","metadata":{"id":"tA8b0em9TL3V"}},{"cell_type":"code","source":"df_files = [raw_shuffle_data_path +f'train_k{k}.csv' for k in range(divide_shuffles)]\nprint(\"학습을 위해 준비된 파일 수 : \", len(df_files))\ninput_shape = (128,128,3)\ngen_data_check = DoodelGenerator(df_files, input_shape=input_shape\n            ,batchsize=25,state='DataCheck',lw=2,last_drop_r=0.2,mixup_r= 0.1)\nimport matplotlib.pyplot as plt\nxx, y = gen_data_check.__getitem__(0)\nfig, axs = plt.subplots(5, 5, figsize=(10,10))\nlabels = np.argmax(y,axis=1) # generator에서 출력한 labels\nfor i in range(25):\n    axs[i//5][i%5].imshow(xx[i]) \n    axs[i//5][i%5].axis('off')\n    axs[i//5][i%5].set_title(label_names[labels[i]])\nplt.show()","metadata":{"id":"AhWnowONtU85","outputId":"bcb800d9-72d7-4519-d733-4ac6aecf8947","trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Train","metadata":{"id":"VWGYGsPnmVJw"}},{"cell_type":"markdown","source":"> 이제 만들어진 Generator로 모델 학습을 해보겠습니다.","metadata":{"id":"XTGSEGwV4YqX"}},{"cell_type":"markdown","source":"## Data 분할 및 Train 계획","metadata":{"id":"ZtTxRdlCemIR"}},{"cell_type":"markdown","source":"> 다음 그림과 같이 100개로 나눈 Data Set에서, 1개는 Hold Out Set으로 만들어 Train 및 Validation 하는데 전혀 사용하지 않을 것입니다. Hold Out Set을 따로 빼놓은 이유는 모델 weighted average ensemble을 할 때에 최적 weight를 찾는데 사용하려고 하기 때문입니다. 이런 방식을 hold out이라고도 하고 OOF (Out of Fold)라고도 부릅니다. Train-Validation 과정 중에 한번도 보여주지 않은 Data를 사용함으로 써 ensemble, stacking 모델의 Overfitting을 방지할 수 있습니다. 그리고 학습 모델은 서로 다른 모델을 몇개 학습할 것인데, 이때 사용할 Validation Set을 바꾸고, Train Set도 파일 1개는 바뀌게 할 것입니다. 이렇게 한 이유는 Data Set이 작다면 (학습하는데 몇시간 정도라면) 1개의 모델에 대해서도 Cross Validation 하는게 일반적인 방법이지만, 이 대회의 경우는 1epoch가 작은 모델도 하루가 걸리기 때문에 4개 정도의 모델을 2epoch 정도 만 학습할 것이고, 이때 각기 서로 다른 Train-Validation Set를 사용하겠습니다. 모델도 다르지만, 학습하는 Data Set도 약간 다르기 때문에 각기 모델이 같은 Data Set으로 학습하는 것보다는 서로 상관도가 적을 것입니다. ensemble 및 stacking model은 base가 되는 모델이 서로 상관도가 적을 때 높은 score 이득을 얻을 수가 있습니다.    \n\n> <img src=\"https://1.bp.blogspot.com/-ZUeg4kN-Ajw/XqJ9tKbsMEI/AAAAAAAAlcI/xhtEi_cgvCQMsIg1zQG92H8TRx22_I2DgCLcBGAsYHQ/s1600/train_model1.PNG\"  width=\"650\" alt=\"doodle/generator\">\n\n> 그리고 여기서는 모델을 1epoch 정도만 학습하고 중단하였지만, Single 모델로도 1epoch 학습 후 Learder board에서 은메달 정도 획득하는 점수가(0.945) 나오고, 1.5epoch 학습 후에는 금메달(0.951) 수준의 점수가 나옵니다.  학습한 weight는 다음 link에 올려져 있으니, 실습해 보실때 이어서 학습하시면 시간을 절약하실 수 있고, 모델 학습 과정을 skip하시고 이후 과정을 진행하실 수도 있겠습니다.\n\n> https://www.kaggle.com/ttagu99/doodle-model\n\n\n> 학습 종료 후에는 그림 1 21처럼 \"ho_prop_model1.npy\", \"test_prob_model1.npy\", \"submission_model1.csv\" 3가지 file을 학습 결과물로 저장할 것입니다. submission 파일은 최종 제출할 파일은 아니지만, 모델1이 얼마나 잘 학습되었는지 Leader Board 순위 확인 용으로 만든 파일이고, \"ho_prop_model1.npy\" 파일은 Hold Out Set을 모델1이 예측해서 나오는 Category별 확률 값입니다. \"test_prob_model1.npy\"은 Test Set을 예측해서 나오는 Category별 확률 값입니다. 4개 모델을 위 과정을 통해서 학습 후에 이 파일들을 이용해서 ensemble하여 최종 예측 파일을 만들겠습니다. 1개 모델을 학습한 후에는 모델을 변경하고 validation, train set를 변경해서 다른 모델을 다음 그림과 같이 이어서 학습하도록 하겠습니다. \n\n> <img src=\"https://1.bp.blogspot.com/-MwG9VPoeOQQ/XqKprV7YZBI/AAAAAAAAlco/z5MT7XbtJrYKUWASIIlIabjebiqcSjO3QCLcBGAsYHQ/s1600/train_model2.PNG\"  width=\"650\" alt=\"doodle/generator\">\n","metadata":{"id":"iW6h_3cMkrWy"}},{"cell_type":"code","source":"train_vals = df_files[:-1]\nhold_out_set= df_files[-1:]\nprint('hold_out_set:',len(hold_out_set), 'train_val_set:',len(train_vals))\n\nR_EPOCHS = 1 # 2 실제 Epoch, 1Epoch 만 학습함\nEPOCHS = R_EPOCHS * (len(train_vals)-1)\nprint('Real Epochs:',R_EPOCHS, 'Divide Virtual Epoch:', EPOCHS)","metadata":{"id":"hIsTnMfxD7nr","outputId":"20b7ca66-7b98-4dd1-a5ea-f3f3effde2fa","trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Base Model","metadata":{"id":"iNth7Gwmnp7Q"}},{"cell_type":"markdown","source":"> 다음 \"build_model\" 함수는 모델 구조를 만들 함수입니다. backbone이 되는 CNN모델을 앞에 두고 마지막 output을 global average pooling 한 후에 label수 만큼의 unit을 갖는 dense layer를 연결해주었습니다. backbone은 EfficientNetB0,4,5,7 4개를 사용하겠습니다. 모델을 이렇게 선택한데에는 특별한 이유는 없으며, 저는 보통 가벼운 모델로 학습-제출까지 한번 할 수 있게 하고(pipeline을 만든다 라고도 함), 그 후 여유가 있을 때 좀 더 큰 모델을 학습합니다.","metadata":{"id":"d02xZcfObnXK"}},{"cell_type":"code","source":"!pip install efficientnet\nimport efficientnet.tfkeras as efn\ndef build_model(backbone= efn.EfficientNetB0\n                ,input_shape = (128,128,3),use_imagenet = 'imagenet'):\n    base_model = backbone(input_shape=input_shape\n                          ,weights=use_imagenet,include_top= False)\n    x = base_model.output\n    x = tf.keras.layers.GlobalAvgPool2D(name='gap')(x)\n    predictions = tf.keras.layers.Dense(len(label_names),activation='softmax'\n                    ,name='prediction')(x)\n    model = tf.keras.Model(inputs=base_model.input, outputs=predictions)\n    return model","metadata":{"id":"L0JusPWRtU89","trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Metric","metadata":{"id":"LV997td_e4rq"}},{"cell_type":"markdown","source":"> 이전 평가 지표에서 설명한 metric에 대해, Train 종료 후 Hold Out Set에 대해 계산할 \"mapk\"함수, 그리고 submission 파일을 만들 때 상위 3개 category label을 return 해줄 \"preds2catids\" 함수, train 과정 중에 top3 accuracy를 확인하기 위한 \"top_3_accuracy\" 함수, MAP@3를 확인하기 위한 \"map_at3\" 함수를 만들겠습니다. \"map_at3\" 함수를 다음과 같이 “top_k_categorical_accuracy”함수를 사용해서 구현한 이유는 굳이 3개 예측 값에 대해 precision을 계산하지 않아도 계산 결과가 같기 때문입니다.\n","metadata":{"id":"Bm-JjeK_fHpJ"}},{"cell_type":"code","source":"def mapk(actual, predicted, k=3): # 학습 후 hold out set 을 평가 하는데 사용할 함수\n    return np.mean([apk(a, p, k) for a, p in zip(actual, predicted)])\n\ndef preds2catids(predictions): # submission을 위해 top3 category로 변환할 함수\n    return pd.DataFrame(np.argsort(-predictions,axis=1)[:,:3]\n                        ,columns=['a','b','c'])\n    \ndef top_3_accuracy(y_true, y_pred):\n    return tf.keras.metrics.top_k_categorical_accuracy(y_true, y_pred, k=3)\n    \ndef map_at3(y_true, y_pred): # train 과정 중에 평가를 위한 함수\n    map3 = tf.keras.metrics.top_k_categorical_accuracy(y_true, y_pred, k=1)*0.5\n    map3 += tf.keras.metrics.top_k_categorical_accuracy(y_true, y_pred, k=2)*0.17\n    map3 += tf.keras.metrics.top_k_categorical_accuracy(y_true, y_pred, k=3)*0.33    \n    return map3","metadata":{"id":"CoF2Z6U8tU86","trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{"id":"UpJrYrb2Iw6k"},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Hyper Parameter Recipes","metadata":{"id":"-Tjt23vfnkJ5"}},{"cell_type":"markdown","source":"> 모델을 하나만 학습한다면 다음과 같은 코드는 필요 없겠지만, 여러 개를 학습할 것임으로 아래와 같이 train 할 모델에 필요한 hyper parameter들을 기록한 다음 반복 수행할 것입니다. 모델에 따라 가능한 batch size, 저장할 이름, 그리고 validation 파일의 번호를 지정하였습니다. input shape는 여기서는 128x128 size에 획의 두께는 2로 통일하였는데, 다른 이미지 사이즈에 line 두께도 서로 다르게 해서 모델을 학습한다면 최종 점수는 더 높아 질 것으로 예상합니다.","metadata":{"id":"B1wpfPlkmcuu"}},{"cell_type":"code","source":"recipes = []\nrecipes.append({'backbone':efn.EfficientNetB0,\"batch_size\":100\n        ,'name':'Efb0','val_sel':0,'input_shape':(128,128,3),'lw':2})\nrecipes.append({'backbone':efn.EfficientNetB4, \"batch_size\":40\n        ,'name':'Efb1','val_sel':4,'input_shape':(128,128,3),'lw':2})\nrecipes.append({'backbone':efn.EfficientNetB5, \"batch_size\":40\n        ,'name':'Efb2','val_sel':5,'input_shape':(128,128,3),'lw':2})\nrecipes.append({'backbone':efn.EfficientNetB7, \"batch_size\":28\n        ,'name':'Efb7','val_sel':7,'input_shape':(128,128,3),'lw':2})","metadata":{"id":"EleZY0IEtU88","trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Save Hold Out Set Probability ","metadata":{"id":"mao9mGBxGgQb"}},{"cell_type":"markdown","source":"> 1개 모델을 학습하고 나서는 해당 모델에 대한 hold out set probability와 test set의 probability가 필요하기 때문에, 다음과 같이 학습된 모델로 hold out set과 test set을예측 후 numpy array로 저장하는 함수를 먼저 만들었습니다.","metadata":{"id":"Wnx8smctG2mk"}},{"cell_type":"code","source":"def make_sub(model, test_datagen, holdout_datagen, model_name):\n   # ho_prob save\n    ho  = model.predict(holdout_datagen,verbose=1) \n    ho_df = holdout_datagen.get_cur_df()\n    ho = ho[:len(ho_df)]\n    top3 = preds2catids(ho)\n    ho_map3 = mapk(ho_df.y, np.array(top3)) # Hold Out 점수 계산\n    np.save(data_path+'output/ho_prob_'+model_name+str(ho_map3)+'.npy',ho)\n\n    # test_prob save\n    test_predictions = model.predict(test_datagen, verbose=1)\n    test_predictions = test_predictions[:len(test_raw_df)]\n    top3 = preds2catids(test_predictions)\n    id2cat = {k: cat.replace(' ', '_') for k, cat in enumerate(label_names)}\n    top3cats = top3.replace(id2cat)\n    np.save(data_path+'output/test_prob_' \n            +model_name+str(ho_map3)+'.npy',test_predictions)    \n\n    # single model test submission    \n    sub_df['word'] = top3cats['a'] + ' ' + top3cats['b'] + ' ' + top3cats['c']\n    submission = sub_df[['key_id', 'word']]\n    submission.to_csv(data_path+'output/submission_'  \n                      +model_name+str(ho_map3)+'.csv', index=False)","metadata":{"id":"4rdVPXXGtU9G","trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"> 학습하는 메인 코드는 다음과 같습니다. 먼저 정의한 모델들에 대해 for loop를 돌며 학습하고, 학습이 완료된 후에는 hold out set과 test set의 probability를 저장합니다. 이때 Learning Rate는 특별 히 Search 하지 않고 default Learning Rate 인 0.001을 사용했고, validation 시 마다 MAP@3 score값이 증가하면 weight save를 하였고, 10 virtual epoch 동안 score가 증가하지 않으면 Learning Rate를 0.5 곱해 주었습니다. 다른 딥러닝 모델들을 학습 해 보신 경험이 있으시다면, Learning Rate 감소가 너무 보수 적이게 되어 있다고 느낄 수 있습니다. 하지만 지금 validation 하는 주기가 1 virtual epoch(실제 Epoch의 1/100 수준)이기 때문에 오히려 빠르게 감소하게 만든 것입니다. 그리고 학습하기 전에 train file 리스트를 한번 shuffle하는 이유는 1개 모델을 1 real epoch 학습하는데 걸리는 시간이 24시간~48시간 이기 때문에, 다른 컴퓨터 사용이나, 예기치 않게 학습이 중단될 경우 파일 리스트를 같은 순서로 학습해서 overfitting 되는 문제를 완화하기 위한 것입니다. 이 코드를 수행하면 계획한 모델들에 대해 각각 probability, submission 파일 들이 저장됩니다. 직접 local에서 학습하기 어려우신 분들을 위해, 1Epoch, 2Epoch만 학습한 모델들로 추가 학습 없이 Single Model에 대한 예측 및 제출까지 바로 가능하도록 Kaggle Notebook 링크를 만들었습니다. 다음의 링크를 참고해주세요.록 Kaggle Notebook 링크를 만들었습니다. 다음의 링크를 참고 해주세요. \n\n> https://www.kaggle.com/code/ttagu99/prediction-single-model-2epoch\n\n","metadata":{"id":"V_O0_CfZNkYl"}},{"cell_type":"code","source":"import random\n\nstrategy = tf.distribute.MirroredStrategy(  \n                tf.config.experimental.list_logical_devices('GPU')) \ngpus= strategy.num_replicas_in_sync  # 가용 gpu 수 \nprint('use gpus:',gpus)\n\n# val map_at3 score 가 virtual epoch 10회 연속 증가 하지 않으면, LR 1/2 감소\nreduce_lr = tf.keras.callbacks.ReduceLROnPlateau(monitor='val_map_at3', factor=0.5\n            ,verbose=1, patience=10,cooldown=5, min_lr=0.00001,min_delta=0.00001) \n\nfor i, recipe in enumerate(recipes):\n    model_name = recipe[\"name\"] + '_val_' + str(recipe[\"val_sel\"]) \n    best_save_model_file = model_name + '.h5' \n    print('best_save_model_file path : ',best_save_model_file)\n    check_point=tf.keras.callbacks.ModelCheckpoint(monitor='val_map_at3',verbose=1\n        ,filepath=data_path + best_save_model_file,save_best_only=True,mode='max') \n\n    valid_file = train_vals[recipe[\"val_sel\"]]\n    train_files = list(np.delete(train_vals, recipe[\"val_sel\"]))    \n    # list 를 Shuffle, 중단후  동일 파일 재학습 하여 Overfitting 되는 문제 완화\n    random.shuffle(train_files) \n    # Train Set만 augmentation 적용\n    train_datagen = DoodelGenerator(train_files,input_shape=recipe['input_shape']\n                ,lw=recipe['lw'], last_drop_r=0.2 , mixup_r=0.1\n                , batchsize=recipe['batch_size']*gpus,state='Train') \n    valid_datagen = DoodelGenerator([valid_file],input_shape=recipe['input_shape']\n                ,lw=recipe['lw'],batchsize=recipe['batch_size']*gpus,state='Valid')\n    \n\n    with strategy.scope():    # 가용 gpu 수 모두 사용\n        model = build_model(backbone= recipe['backbone']\n        ,input_shape=recipe['input_shape'], use_imagenet = 'imagenet')\n        model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.001*gpus)\n        ,loss='categorical_crossentropy'\n        ,metrics=[tf.keras.metrics.categorical_accuracy, top_3_accuracy, map_at3]) \n    # 동일 모델이름의 사전 학습된 weights가 있다면 로딩 후 재시작   \n    if os.path.exists(data_path + best_save_model_file): \n        print('restart train : ', data_path + best_save_model_file)\n        model.load_weights(data_path + best_save_model_file)\n    hist = model.fit(train_datagen\n        ,validation_data=valid_datagen,epochs= EPOCHS,verbose=1\n        ,callbacks =[reduce_lr, check_point, OnEpochEnd(train_datagen.on_epoch_end)])\n    # 학습 완료 후 Best Score Model 로딩\n    model.load_weights(data_path + best_save_model_file) \n    test_datagen = DoodelGenerator([data_path+\"test_raw.csv\"]\n                            ,input_shape=recipe['input_shape'],lw=recipe['lw']\n                            , batchsize=recipe['batch_size']*gpus,state='Test')\n    holdout_datagen = DoodelGenerator(hold_out_set,input_shape=recipe['input_shape']\n                            ,lw=recipe['lw']\n                            , batchsize=recipe['batch_size']*gpus,state='Holdout')\n    # hold out & test set, probability 파일 저장\n    make_sub(model, test_datagen, holdout_datagen, model_name) ","metadata":{"id":"0g079USJtU9L","outputId":"3a84004c-882b-4c35-8b9a-93b541147538","trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Ensemble","metadata":{"id":"wMITV7LyH-2Q"}},{"cell_type":"markdown","source":"> 이전 과정에서 학습한 모델 들로 hold out set, test set의 probability 들이 저장되어 있습니다. 해당 파일을 loading하여 각 probability들의 weighted average ensemble을 진행하겠습니다. CNN 모델을 학습 없이 ensemble을 위한 과정만 실습을 해보시려면 아래 kaggle notebook을 통해서 실행할 수 있습니다. \n> https://www.kaggle.com/code/ttagu99/ensemble-models-2epoch","metadata":{"id":"qX3fXP32IWi8"}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\noutput_path = data_path+'output/'\noutputs = os.listdir(output_path)\n # hold out probability\nhold_out_probs = [output_path+f for f in outputs if f.find('ho_prob') >= 0 ]\n# test probability\ntest_out_probs = [output_path+f for f in outputs if f.find('test_prob') >= 0 ] \n\nho_df = pd.read_csv(hold_out_set[0])\nho_s = []\nfor prob_path in hold_out_probs:\n    ho = np.load(prob_path)\n    ho = ho[:len(ho_df)]\n    ho_s.append(ho)\ntargets = ho_df.y.to_numpy() # hold out target","metadata":{"id":"bWQ9Qmm7tU9U"},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Optimizer for weight ensemble ","metadata":{"id":"QNp865E-IzrW"}},{"cell_type":"markdown","source":"> 각 모델의 probability의 최적 weight를 찾기 위해 경사 하강법을 구현한 함수는 다음과 같습니다. step size 만큼 weight들을 이동하며 loss가 줄어드는 방향으로 조절하게 하였습니다. 이 때 loss 함수는 1 - MAP@3를 사용하였습니다. ","metadata":{"id":"_GSFt5DqMtSC"}},{"cell_type":"code","source":"from scipy.optimize import OptimizeResult\ndef map3_loss(weights, predictions, targets):\n    final_prediction = 0\n    for weight, prediction in zip(weights, predictions):\n            final_prediction += weight*prediction\n    top3 = np.argsort(-final_prediction, axis=1)[:, :3]\n    ho_map3 = mapk(targets, np.array(top3))    \n    print(weights, ho_map3)\n    return 1 - ho_map3\n\ndef custom_minimizer(fun,x0,args=(),stepsize=0.1,maxiter=100,callback=None):\n    bestx = x0\n    besty = fun(x0,*args) # loss func\n    funcalls = 1\n    niter = 0\n    improved = True\n    stop = False\n\n    while improved and not stop and niter < maxiter: \n        improved = False\n        niter += 1\n        for dim in range(np.size(x0)):\n            # 각 prob에 해당하는 weight를 step size만큼 +,- 방향 이동한 곳의 \n            # loss 계산하고, 가장 줄어드는 방향으로 업데이트\n            for s in [bestx[dim] - stepsize, bestx[dim] + stepsize]: \n                testx = np.copy(bestx)\n                testx[dim] = s\n                minx = min(testx)\n                if minx<0: \n                    testx-=minx  \n                testx/=sum(testx)\n                testy = fun(testx, *args) \n                funcalls += 1\n                if testy < besty: \n                    besty = testy\n                    bestx = testx\n                    improved = True\n            if callback is not None:\n                callback(bestx)\n    return OptimizeResult(fun=besty, x=bestx, nit=niter, success=(niter > 1)) \n\nstarting_values = [0.25]*len(ho_s) # 균등 분할 부터 Start\nres = custom_minimizer(map3_loss,starting_values, args=(ho_s, targets))\nprint('Best Ensamble Score: ', 1-res['fun'])\nprint('Best Weights:', res['x'])","metadata":{"id":"SfDjvQ0SkUpT"},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{"id":"so_jYjUxkUpU","outputId":"b47aa0c2-270c-42f5-b37b-b1de3b4fb840"},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## submission1","metadata":{"id":"7il_j_d-JLfz"}},{"cell_type":"markdown","source":"> ensemble weight optimizer 가 찾은 Best Weights로 test probability 들을 weighted average 하여 최종 probility를 계산하고, top3를 추출해서 최종 제출 파일을 생성하겠습니다.","metadata":{"id":"Y8JkcK9ual91"}},{"cell_type":"code","source":"def ens_sub(ens_prob, file_name='ens_sub.csv'):\n    top3 = preds2catids(ens_prob)\n    id2cat = {k: cat.replace(' ', '_') for k, cat in enumerate(label_names)}\n    top3cats = top3.replace(id2cat) \n    sub_df['word'] = top3cats['a'] + ' ' + top3cats['b'] + ' ' + top3cats['c']\n    submission = sub_df[['key_id', 'word']]\n    submission.to_csv(data_path+'output/'+ file_name, index=False)\n    \nens_prob = np.zeros((len(sub_df),len(label_names)))\nfor i, prob_path in enumerate(test_out_probs):\n    ens_prob += (np.load(prob_path) * res['x'][i])\n    \nens_sub(ens_prob)","metadata":{"id":"tMj3FnlKL6--"},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{"id":"gp8mUCwFL6_A"},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## submission2","metadata":{"id":"Ak658MJRL6_B"}},{"cell_type":"markdown","source":"> 대회 중에는 미처 알지 못했지만, 대회가 끝난 후 1등 팀이 solution을 공개했을 때 test set의 분포에 magic이 있었음이 알려 졌습니다. 카테고리 별 예측의 수를 고르게 balancing해서 하면 score가 올라간다는 것입니다. 거기에 대한 hint는 test set의 개수에서 포착되었습니다. test set이 112199개로, label 숫자인 340개로 나누면 나머지가 339개로 1개 부족합니다. test set의 label 분포가 고르게 있을 것이라는 의심이 들게 됩니다. 그런데 이런 magic이 이 대회 한번만 있었던 일은 아닙니다. test set의 분포를 알 수 있는 hint가 어느 정도 있다면, 최종 제출 파일을 2개 선택할 수 있기 때문에, 하나 정도는 overfitting을 감안하고서 선택할 수도 있겠습니다. 2개를 모두 같은 방식으로 하는 것은 test set 분포가 예측과 다를 때에는 망할 수 있기 때문에 둘 중 하나는 정상적인 결과 물을, 하나는 test set 분포를 활용해 post processing된 결과물을 제출하겠습니다.","metadata":{"id":"8eH4vuMZL6_B"}},{"cell_type":"code","source":"from collections import Counter,OrderedDict\nfrom operator import itemgetter\ndef balancing_predictions(test_prob,factor=0.1,minfactor=0.001,patient=5\n                          ,permit_cnt=332,max_search=10000,label_num=340):\n    maxk = float('inf')\n    s_cnt = np.zeros(label_num)\n    for i in range(max_search):\n        ctop1 = Counter(np.argmax(test_prob,axis=1))\n        ctop1 = sorted(ctop1.items(), key=itemgetter(1), reverse=True)\n        if maxk > ctop1[0][1]:\n            maxk = ctop1[0][1]\n        else:\n            s_cnt[ctop1[0][0]]+=1\n            if np.max(s_cnt)>patient:\n                if factor< minfactor:\n                    print('stop min factor')\n                    break\n                s_cnt=np.zeros(label_num)\n                factor*=0.99\n                print('reduce factor: ',factor,', current max category num: '\n                ,ctop1[0][1])\n\n        if ctop1[0][1] <= permit_cnt:\n            print('idx: ',ctop1[0][0] ,', num: ', ctop1[0][1]) \n            break\n        test_prob[:,ctop1[0][0]] *= (1.0-factor)\n    return test_prob\n\nbal_ens_prob = balancing_predictions(ens_prob)\nens_sub(bal_ens_prob,'bal_ens_sub.csv')","metadata":{"id":"Jmp7iwr3L6_E","outputId":"c7f3de8a-f812-4eff-cedf-6d741276ca05","executionInfo":{"status":"error","timestamp":1588600566747,"user_tz":-540,"elapsed":1607,"user":{"displayName":"은주니","photoUrl":"https://lh3.googleusercontent.com/a-/AOh14GhyUBzKneZcIle1t7HXIAaL548lK9ndvDFCUhROwQ=s64","userId":"15525163033989858669"}}},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 맺음말","metadata":{"id":"O_07s4KyJQnr"}},{"cell_type":"markdown","source":"> 저는 지금까지 한번도 끝난 대회를 복기하려 하지 않았고, 캐글 할 시간이 조금이라도 나면 새로운 대회를 해야겠다라는 마음만 가지고 있었습니다. 이 책을 쓰며 그것이 잘못된 생각이 었다는 것을 깨달았습니다. 나 자신이 만든 코드에, 많은 오류가 있었고, 그 오류를 만들지 않았다면 훨씬 더 좋은 점수를 낼 수 있었다는 것을 알았습니다. 하지만 실수 했던 오류들을 복기 하지 않았으니, 비슷한 대회에서 같은 오류를 범하고 있었습니다. 들어간 시간에 비해 실력이 많이 안늘었겠구나 라는 생각이 듭니다. 이전에 캐글하는 시간 분배가 새로운 대회:끝난 대회 10:0 이었다면, 9:1 정도는 배분해서 끝난 대회를 복기하며 하나의 대회를 마무리 해야겠습니다.\n\n> 여러분도 혹시나 저와 같은 생각을 가지고 계시다면, 직접 참여한 대회에 대해 복기하는 시간을 어느 정도는 분배하시는 것을 추천 드립니다. 대한민국 캐글러 화이팅!!!","metadata":{"id":"YLcy-QICJZLt"}},{"cell_type":"code","source":"","metadata":{"id":"FbjEoRDSJX9H"},"execution_count":null,"outputs":[]}]}