GPT-2で作るJoke BotからSlack Botまで #3 コードの編集

# イントロ
この記事は主に自分のパソコンの上じゃなくてColaboratoryで深層学習をしたいという人向けです。もちろん、もし自分のパソコンでしてみたい方々がいてもできるようにやり方を書きます。
よってJoke　Botを作りたい人が対象者といっても誤りはないかもしれません。この内容は全部[このノートブック](https://github.com/isamu-isozaki/toxic-joke-generator/blob/master/GPT_Jokes.ipynb)と[このpython file](https://github.com/isamu-isozaki/toxic-joke-generator/blob/master/train.py)にあるのでぜひ参考までに見てください。Part 1 と Part 2 は[ここ](https://qiita.com/isamuIsozaki/items/23b2d2d937fe27a523a0)と[ここ](https://qiita.com/isamuIsozaki/items/01b1bd9bd741ce2b1199)にあるのでぜひ最初に見ておいてください。
# トレーニングをする前に
いきなりテキストファイルからは学習できません。なのでエンコーディングしないといけません。これは

     python encode.py input_text_file.txt output_data.txt.npz
とすればできます。実際にどのようにエンコーディングしているかは英語ですが[これ](https://medium.com/@isamu.website/understanding-the-gpt-2-source-code-part-2-4a980c36c68b)に書いたので興味あればぜひ見てみてください！

# Colaboratoryとは?
ColaboratoryはGoogleがくれる無料のGPUが付いているjupyter notebookです。前までは結構遅いGPUでしたが最近はT4という結構速いGPUが付き始めたのでよくなりました。google driveでjupyter notebookを開こうとするとできます。
# 欠点
ただ、ちょっとした欠点もあります。学習したモデルが簡単に保存できないというのと１２時間でどんな作業をしているかは関係なくノートブックが初期化されてしまうことです。つまり、モデルのデーターが削除されやすいわけです。よって、ふつうはColaboratoryは単純なテストなどに使われます。例えば数時間モデルを学習した結果を報告することなどに使われて数日や数か月にわたる学習などには大して使われません。これを解決する方法はGoogle　Driveにモデルを保存することです。こうすると次回トレーニングが終わったところから始めるにしても簡単に続けられ、何日間も何週間にわたっても学習し続けることができます。このためにpydriveというライブラリを使います。
# ColaboratoryとGoogle Driveのつなげ方
正直言うと初めてこれをしようとしたときはうまくいきませんでした。結構いろいろしなくてはいけないことなどがあってほかの人のコードを見る以外勉強する方法がありませんでした。よって、とりあえずここにpydriveを使う方法を書いておきます。
1. ユーザー認証
一番最初にしないといけないことはユーザー認証です。これは

    from pydrive.auth import GoogleAuth
    from pydrive.drive import GoogleDrive
    from google.colab import auth
    from oauth2client.client import GoogleCredentials

    # 1. Authenticate and create the PyDrive client.
    auth.authenticate_user()
    gauth = GoogleAuth()
    gauth.credentials = GoogleCredentials.get_application_default()
    drive = GoogleDrive(gauth)

と入れればできます。これがやることはclient.jsonというファイルを保存することです。これがないとgoogle driveに何の操作もできません。しかし、client.jsonというファイルはセキュリティーの都合上などで何度も何度も突然消えてしまうので私はgoogle driveに操作するときは毎回このコードを動かしています。
2. Google Driveの必要なファイルを全部取得する
これは[個々のコードが元です](https://github.com/gsuitedevs/PyDrive/issues/16).
単純に

    def ListFolder(parent, show=False):
      filelist=[]
      file_list = drive.ListFile({'q': "'%s' in parents and trashed=false" % parent}).GetList()
      for f in file_list:
        if f['mimeType']=='application/vnd.google-apps.folder': # if folder
             if f['title'] == model_folder_name or f['title'] == past_folder_name:          
                 filelist.append({"id":f['id'],"title":f['title'], "mimeType": 'application/vnd.google-apps.folder', "list":ListFolder(f['id'])})
             else:
                 for f1 in ListFolder(f['id']):
                      filelist.append(f1)
        else:
            filelist.append({"id":f['id'],"title":f['title'],"mimeType":"file",\
            "title1":f['alternateLink']})
      return filelist
    fl = ListFolder('root')
という感じにすれば全部とれます。ここでmodel_folder_nameとpast_folder_nameは欲しいフォルダーの名前です。別に抜いても大丈夫ですがそうすればすべてのフォルダーのすべてのデータがflに入るのでそれは少しいやだなーと思ってやりました。ここで作ったflというのはあとでGoogle driveからファイルをとったりするときに使えます。
3. Folderを取得する
例えば実際にフォルダーをGoogle driveからとる際には

	for f in fl:
	  if f['title'] == model_folder_name:
	    for part in f["list"]:
	      fil = drive.CreateFile({'id':part['id']})
	      fil.GetContentFile(part['title'])
	      os.rename(part['title'], "./run1/"+part['title'])

のようにすればフォルダーの中のファイルが全部run1に保存されます。fil.GetContentFileは欲しいフォルダーの名前を付けてgoogle driveのファイルを取得します。
ただ、フォルダーをアップロードする都合上すこし名前が変になってしまいまして、

	for f in fl:
	  if f['title'] == model_folder_name:
	    for part in f["list"]:
	      fil = drive.CreateFile({'id':part['id']})
	      fil.GetContentFile(part['title'][18:])
	      os.rename(part['title'][18:], "./run1/"+part['title'][18:])
としないといけませんでした。
4. フォルダーをアップロードする

	for content in os.listdir(parent_folder):
					f = drive.CreateFile({"parents": [{"kind": "drive#fileLink", "id": google_drive's parent_folder id}]})
					f.SetContentFile(parent_folder+"/"+content)
					f.Upload()
見ての通り.SetContentFileの段階でparent_folder + "/" + contentとファイルを指定するのですが、Google Driveにアップロードしたファイルcontentという名前ではなくparent_folder + "/" + contentと名前を付けるので[18:]とファイルを取得する際にparent_folder+"/"を切り捨てる作業をしないといけませんでした。もし解決策を知っている方がいたらぜひ教えてください。
# train.pyの編集
ほとんどの[このノートブック](https://github.com/isamu-isozaki/toxic-joke-generator/blob/master/GPT_Jokes.ipynb)のコードは結構簡単なので説明はもしリクエストがあれば答えますが今は長くしたくないので省いておきます。
よって、一気にtrain.pyをどのように編集したか説明します。train.pyは

    python ./train.py --dataset jokes_400_2_3.0_1.txt.npz --batch_size 2 --sample_every 100 --save_every 1000

とすることによって、トレーニングが始まります。しかし、少し残念ながらGoogle Driveに保存されません。よって、少しコードを変えて
    
    !python ./train.py --dataset jokes_400_2_3.0_1.txt.npz --batch_size 2 --sample_every 100 --save_every 1000 \
    --model_folder_name $model_folder_name --past_model_folder_name $past_folder_name
としてmodel_folder_nameというGoogle Driveのフォルダーに今のモデル、past_folder_nameというGoogle Driveのファイルに過去もモデルと今のモデルを保存するようにしました。
[このノートブック](https://github.com/isamu-isozaki/toxic-joke-generator/blob/master/GPT_Jokes.ipynb)に行けば変更できますが初期の値は"model"と"past"です。これらのフォルダーは最初からGoogle Driveで作っておく必要があります。
変更したところはsaveという関数です！ここで毎回保存するようにコードができていたのでここでアップロードするようにしました。

    def get_folder_id(name):
			global fl
			for i in fl:
				if i["title"] == name:
					return i['id']
		def get_children(name):
			global fl
			for i in fl:
				if i["title"] == name:
					return i.get("list", [])
		def save():
			global fl
			for content in os.listdir(os.path.join(f"./{CHECKPOINT_DIR}", args.run_name)):
				if content[0] != "e":#summary file
					os.remove(os.path.join(f"./{CHECKPOINT_DIR}", args.run_name)+"/"+content)
			maketree(os.path.join(CHECKPOINT_DIR, args.run_name))
			print(
				'Saving',
				os.path.join(CHECKPOINT_DIR, args.run_name,
							 'model-{}').format(counter))
			saver.save(
				sess,
				os.path.join(CHECKPOINT_DIR, args.run_name, 'model'),
				global_step=counter)
			with open(counter_path, 'w') as fp:
				fp.write(str(counter) + '\n')
			from pydrive.auth import GoogleAuth
			from pydrive.drive import GoogleDrive
			from google.colab import auth
			from oauth2client.client import GoogleCredentials

			# 1. Authenticate and create the PyDrive client do this every time because sometimes the client.json just dissapears.
			auth.authenticate_user()
			gauth = GoogleAuth()
			gauth.credentials = GoogleCredentials.get_application_default()
			drive = GoogleDrive(gauth)
			#Delete folders from model_folder_name
			for file in get_children(args.model_folder_name):
				f = drive.CreateFile({"id": file["id"]})
				f.Delete()
			#Upload to model_folder_name
			for content in os.listdir(os.path.join(f"./{CHECKPOINT_DIR}", args.run_name)):
				f = drive.CreateFile({"parents": [{"kind": "drive#fileLink", "id": get_folder_id(args.model_folder_name)}]})
				f.SetContentFile(os.path.join(f"./{CHECKPOINT_DIR}", args.run_name)+"/"+content)
				f.Upload()
			#Upload to past_model_folder_name
			for content in os.listdir(os.path.join(f"./{CHECKPOINT_DIR}", args.run_name)):
				f = drive.CreateFile({"parents": [{"kind": "drive#fileLink", "id": get_folder_id(args.past_model_folder_name)}]})
				f.SetContentFile(os.path.join(f"./{CHECKPOINT_DIR}", args.run_name)+"/"+content)
				f.Upload()
			#update folder list
			fl = ListFolder("root")
という感じです。ちょっと説明が苦手なのでコードを一気に載せてすみません。コードにしたら結構でかいですがしたことは単純に
1. model_folder_nameのGoogle Driveのフォルダーのファイルを全部削除する。
2. model_folder_nameとpast_foler_nameのフォルダーにファイルをアップロードする。
3. File listを更新する
だけです。もしわからないことや変なところなどがあればぜひ教えてください！ちょっと長くなったので次回実際にトレーニングの結果を報告します。