# 透過 Django 架設網頁 ## 安裝 Django ``` pip3 install django ``` ## 創建專案 ``` django-admin startproject web_demo_tutorial cd web_demo_tutorial ``` 當前文件夾結構如下 ``` . ├── manage.py └── web_demo_tutorial ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py 2 directories, 6 files ``` ## 創建 Inference 應用 ``` python3 manage.py startapp inference ``` 當前文件夾結構如下 ``` . ├── inference │   ├── __init__.py │   ├── admin.py │   ├── apps.py │   ├── migrations │   │   └── __init__.py │   ├── models.py │   ├── tests.py │   └── views.py ├── manage.py └── web_demo_tutorial ├── __init__.py ├── __pycache__ │   ├── __init__.cpython-310.pyc │   └── settings.cpython-310.pyc ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py 5 directories, 15 files ``` ## 註冊 Inference 應用 開啟 ./web_demo_tutorial/settings.py,並將應用名加入(第34行)。 ```python=33 INSTALLED_APPS = [ 'inference', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ] ``` 同時修改 TEMPLATES(第58行)。 ```python=55 TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': ['templates'], #修改此行 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] ``` ## 使用模型 設定 ./inference/models.py ```python= from django.db import models class Sentence(models.Model) : EMOTION_LIST = ( ('sadness', 'sadness'), ('joy', 'joy'), ('love', 'love'), ('anger', 'anger'), ('fear', 'fear'), ('surprise', 'surprise'), ) input_text = models.CharField(max_length=200) old_model_answer = models.CharField(max_length=200) new_model_answer = models.CharField(max_length=200) fix_answer = models.CharField(max_length=200, choices=EMOTION_LIST) old_probability_list = [] new_probability_list = [] def __str__(self) : return f'{self.input_text} {self.old_model_answer} {self.new_model_answer} {self.fix_answer} {self.old_probability_list} {self.new_probability_list}' ``` 接著同步資料庫 ``` python3 manage.py makemigrations ``` ``` Migrations for 'inference': inference/migrations/0001_initial.py - Create model Sentence ``` 再把 models.py 中的欄位寫入資料庫 ``` python3 manage.py migrate ``` ``` Operations to perform: Apply all migrations: admin, auth, contenttypes, inference, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying auth.0010_alter_group_name_max_length... OK Applying auth.0011_update_proxy_permissions... OK Applying auth.0012_alter_user_first_name_max_length... OK Applying inference.0001_initial... OK Applying sessions.0001_initial... OK (tutorial-env) ➜ web_demo_tutorial git:(master) ✗ ``` 當前文件夾結構如下 ``` . ├── db.sqlite3 ├── inference │   ├── __init__.py │   ├── __pycache__ │   │   ├── __init__.cpython-310.pyc │   │   ├── admin.cpython-310.pyc │   │   ├── apps.cpython-310.pyc │   │   └── models.cpython-310.pyc │   ├── admin.py │   ├── apps.py │   ├── migrations │   │   ├── 0001_initial.py │   │   ├── __init__.py │   │   └── __pycache__ │   │   ├── 0001_initial.cpython-310.pyc │   │   └── __init__.cpython-310.pyc │   ├── models.py │   ├── tests.py │   └── views.py ├── manage.py └── web_demo_tutorial ├── __init__.py ├── __pycache__ │   ├── __init__.cpython-310.pyc │   ├── settings.cpython-310.pyc │   └── urls.cpython-310.pyc ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py 7 directories, 24 files ``` ## 加入 Inference 程式 ./inference/Mylstm.py ```=python import torch import torch.nn as nn from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence from torchtext.data import * from .path import FIELDFILE_ROOT text_field = torch.load(FIELDFILE_ROOT + 'field/text_field.pth') class MyLSTM(nn.Module): def __init__(self, dimension=128): super(MyLSTM, self).__init__() self.embedding = nn.Embedding(len(text_field.vocab), 300) self.dimension = dimension self.lstm = nn.LSTM(input_size=300, hidden_size=dimension, num_layers=4, dropout=0.3, batch_first=True, bidirectional=False) self.drop = nn.Dropout(p=0.3) self.fc = nn.Linear(dimension, 6) def forward(self, text, text_len): text_emb = self.embedding(text) packed_input = pack_padded_sequence(text_emb, text_len, batch_first=True, enforce_sorted=False) packed_output, _ = self.lstm(packed_input) output, _ = pad_packed_sequence(packed_output, batch_first=True) out_forward = output[range(len(output)), text_len - 1, :self.dimension] out_reverse = output[:, 0, self.dimension:] out_reduced = torch.cat((out_forward, out_reverse), 1) text_fea = self.drop(out_reduced) text_fea = self.fc(text_fea) text_fea = torch.squeeze(text_fea, 1) text_out = torch.nn.functional.relu(text_fea) return text_out ``` ./inference/save_load_util.py ```=python import torch import os device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") # Save and Load Functions def save_model(save_path, model, valid_loss, print_prompts=True): if save_path == None: return state_dict = {'model_state_dict': model.state_dict(), 'valid_loss': valid_loss} torch.save(state_dict, save_path) if print_prompts: print(f'Model saved to ==> {save_path}') def load_model(load_path, model, print_prompts=True): if load_path==None: return state_dict = torch.load(load_path, map_location=device) model.load_state_dict(state_dict['model_state_dict']) if print_prompts: print(f'Model loaded from <== {load_path}') def save_metrics(save_path, train_loss_list, valid_loss_list, global_steps_list, print_prompts=True): if save_path == None: return state_dict = {'train_loss_list': train_loss_list, 'valid_loss_list': valid_loss_list, 'global_steps_list': global_steps_list} torch.save(state_dict, save_path) if print_prompts: print(f'Metrics saved to ==> {save_path}') def load_metrics(load_path, print_prompts=True): if load_path==None: return state_dict = torch.load(load_path, map_location=device) if print_prompts: print(f'Metrics loaded from <== {load_path}') return state_dict['train_loss_list'], state_dict['valid_loss_list'], state_dict['global_steps_list'] ``` ./inference/inference.py ```=python import csv import torch import pandas as pd device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") class inf : def get_classification(label) : classification = ['sadness', 'joy', 'love', 'anger', 'fear', 'surprise'] return classification[label] def inference(self, input, model, vocab) : model.eval() sentence = input words = sentence.split() words_index = [vocab[word] for word in words] text = torch.tensor([words_index]).to(device) text_len = torch.tensor([len(text[0])]).long() output = model(text, text_len) _, predicted_label = torch.max(output.data, 1) label = int(predicted_label) score_list = output.data[0].tolist() # i = self.get_classification(label) # 印出情緒結果 probability_list = [score / sum(score_list) for score in score_list] for i in range(6) : probability_list[i] = format( probability_list[i], '.2f' ) # for i in range(6) : print('{:9}- {:.2%}'.format(self.get_classification(i), probability_list[i])) classification = ['sadness', 'joy', 'love', 'anger', 'fear', 'surprise'] return label, probability_list ``` 當前文件夾結構如下 ``` . ├── db.sqlite3 ├── inference │   . │   . │   ├── Mylstm.py ✔️ │   ├── inference.py ✔️ │   └── save_load_util.py ✔️ └── .. 7 directories, 27 files ``` ## 加入網頁首頁 新增 templates 資料夾 ``` mkdir templates ``` 在 ./templates/ 路徑下新增 home.html ```html= <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>首頁</title> <style> @font-face { font-family: 'NotoSerif' ; src: url(https://fonts.google.com/noto/specimen/Noto+Serif+TC?subset=chinese-traditional); } .flex{ display:flex; align-items:center; justify-content:center; position: absolute; width: 100%; height: 100%; background-color:white; } #homepage { border: 1px solid white; width: 30%; height: 300px; text-align: center; background: #F5EBB7; padding: 20px 50px; position: relative; border-radius: 30px; } #homepage button { width: 150px; height: 60px; text-transform: uppercase; border: 3px solid black; text-align: center; font-size: 24px; font-family : 'NotoSerif', serif ; color: black; line-height: 50px; border-radius: 30px; cursor: pointer; background:rgba(0,0,0,0); } .title { font-family : 'NotoSerif', serif ; line-height: 1.5 ; font-size: 48px; } </style> </head> <body> <div class="flex"> <div id="homepage"> <p class="title">Text Classification</p> <button type="bottom" onclick=" location.href='/inference/'" >Enter</button> </div> </div> </body> ``` 於 ./web_demo_tutorial/ 新增 view.py ```python= from django.shortcuts import render def home(request) : return render(request, 'home.html', {}) ``` 修改./web_demo_tutorial/urls.py ```python= from django.contrib import admin from django.urls import path, include from . import views urlpatterns = [ path('',views.home ), path('admin/', admin.site.urls), path('inference/', include('inference.urls') ) ] ``` 當前文件夾結構如下 ``` . ├── db.sqlite3 ├── templates ✔️ │   └── home.html ✔️ ├── web_demo_tutorial │   . │   . │   ├── views.py ✔️ │   └── urls.py ✔️ └── .. 8 directories, 29 files ``` ## 加入輸入頁面、結果展示頁面 ``` cd inference & mkdir templates & cd templates mkdir inference & cd inference ``` 在當前路徑下新增輸入頁面與結果展示頁面 input.html ```html= <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>輸入英文句子</title> <style> @font-face { font-family: 'NotoSerif' ; src: url(https://fonts.google.com/noto/specimen/Noto+Serif+TC?subset=chinese-traditional); } .flex{ display:flex; align-items:center; justify-content:center; position: absolute; width: 100%; height: 100%; background-color:white; } #inputpage { border: 1px solid white; width: 30%; height: 348px; text-align: center; background: #F5EBB7; padding: 20px 50px; position: relative; border-radius: 30px; } #inputpage button { width: 150px; height: 60px; text-transform: uppercase; border: 3px solid black; text-align: center; font-size: 24px; font-family : 'NotoSerif', serif ; color: black ; line-height: 50px; border-radius: 30px; cursor: pointer; background:rgba(0,0,0,0); margin-top: 48px; } .title { font-family : 'NotoSerif', serif ; line-height: 1.5 ; font-size: 48px; } .context { font-family : 'NotoSerif', serif ; line-height: 1.5 ; font-size: 24px; } #inputpage input { width: 350px; border: 3px solid white; font-size: 20px; color: black; font-family : 'NotoSerif', serif ; padding: 5px 10px; } </style> </head> <body> <div class="flex"> <div id="inputpage"> <p class="title">請輸入英文句子</p> <form action="./result/" method="post"> {% csrf_token %} <label class = "context">請輸入英文句子:</label><input type="text" name="input_text"><br /> <button type="submit">送出</button> </form> </div> </div> </body> ``` result.html ```=html= <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>結果</title> <style> @font-face { font-family: 'NotoSerif' ; src: url(https://fonts.google.com/noto/specimen/Noto+Serif+TC?subset=chinese-traditional); } body { background-color: white; } .title { width : 100% ; text-align: center ; font-family: 'NotoSerif', serif ; font-size: 50px ; line-height: 1.1 ; } .subtitle_box { height: 40px; margin-top: 25px; margin-bottom: -10px; padding: 25px; border-radius: 8px; display: flex; align-items: center; justify-content: center; } .subtitle { font-family : 'NotoSerif', serif ; line-height: 1.5 ; font-size: 24px; } .wrapper { overflow: hidden ; display: flex; } .small_title_box { font-family : 'NotoSerif', serif ; width: 33%; margin: 10px; padding: 25px; border-radius: 8px; display: flex; justify-content: center; align-items: center; } .chart_box { width: 33%; margin: 10px; padding: 25px; border-radius: 8px; display: flex; justify-content: center; align-items: center; } .old_model_Title { height: 30px; font-size: 24px; background: #d8eee1; } .new_model_Title { height: 30px; font-size: 24px; background: #f3f7f9; } .dataset_Title { height: 30px; font-size: 24px; background: #fef2f2; } .old_model_Info { height: 30px; font-size: 22px; background: #d8eee1; } .new_model_Info { height: 30px; font-size: 22px; background: #f3f7f9; } .dataset_Info { height: 30px; font-size: 22px; background: #fef2f2; } </style> </head> <body> <div class="wrapper"> <h1 class="title"> <strong>情緒辨識結果 : {{ new_model_ans }}</strong> <p class="subtitle_box subtitle">你輸入的句子為 : {{ input_text }}</p> </h1> </div> <div class="wrapper"> <div class="small_title_box old_model_Title"><strong>模型訓練前的資訊</strong></div> <div class="small_title_box new_model_Title"><strong>模型訓練後的資訊</strong></div> <div class="small_title_box dataset_Title"><strong>資料集的相關資訊</strong></div> </div> <div class="wrapper"> <div class="small_title_box old_model_Info"><p>模型訓練前的辨識結果:{{ old_model_ans }}</p></div> <div class="small_title_box new_model_Info"><p>模型訓練後的辨識結果:{{ new_model_ans }}</p></div> <div class="small_title_box dataset_Info"><p>資料集情緒分佈如下</p></div> </div> <div class="wrapper"> <div class = "chart_box"><canvas id="oldModel"></canvas></div> <div class = "chart_box"><canvas id="newModel"></canvas></div> <div class = "chart_box"><canvas id="datasetNumList"></canvas></div> </div> <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.0.1/chart.min.js" integrity="sha512-tQYZBKe34uzoeOjY9jr3MX7R/mo7n25vnqbnrkskGr4D6YOoPYSpyafUAzQVjV6xAozAqUFIEFsCO4z8mnVBXA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script> const ctx1 = document.getElementById('oldModel'); var df = {{old_probability_list|safe}} var cla = {{classification|safe}} new Chart(ctx1, { type: 'pie', data: { labels: cla, datasets: [{ label: 'Percentage', data: df, borderWidth: 1 }] }, options: { responsive : false, } }) ; </script> <script> const ctx2 = document.getElementById('newModel'); var df = {{new_probability_list|safe}} var cla = {{classification|safe}} new Chart(ctx2, { type: 'pie', data: { labels: cla, datasets: [{ label: 'Percentage', data: df, borderWidth: 1 }] }, options: { responsive : false, } }) ; </script> <script> const ctx3 = document.getElementById('datasetNumList'); var df = {{datasetNumList|safe}} var cla = {{classification|safe}} var per = {{datasetNumPercentage|safe}} new Chart(ctx3, { type: 'pie', data: { labels: cla, datasets: [{ label: 'Percentage', data: per , borderWidth: 1 }] }, options: { responsive : false, } }) ; </script> <div class="right"> <h2><a href="../">重新輸入</a></h2> </div> </body> </html> ``` 回到 web_demo_tutorial 資料夾,當前文件夾結構如下 ``` . ├── db.sqlite3 ├── inference │ . │ . │   └── templates ✔️ │      └── inference ✔️ │      ├── input.html ✔️ │      └── result.html ✔️ └── .. 10 directories, 31 files ``` ## Linking to URLs 於 ./inference/ 路徑新增 urls.py。 ```python= from django.urls import path from . import views app_name = 'inference' urlpatterns = [ path('', views.input, name='input'), path('result/', views.result, name='result'), ] ``` 當前文件夾結構如下 ``` . ├── db.sqlite3 ├── inference │ . │ . │   └── urls.py ✔️ │ └── .. 10 directories, 32 files ``` ## 修改 views 於 ./inference/ 路徑新增 path.py,用於相對路徑。 ```python= import os settings_dir = os.path.dirname(__file__) PROJECT_ROOT = os.path.abspath(os.path.dirname(settings_dir))+ '/' FIELDFILE_ROOT = os.path.abspath(os.path.dirname(settings_dir)) + '/inference/' ``` 修改 ./inference/views.py ```python= from django.shortcuts import render import torch import csv from .path import PROJECT_ROOT, FIELDFILE_ROOT from .Mylstm import MyLSTM from .save_load_util import load_model from .inference import inf from .models import Sentence # new code import pandas as pd # end new code device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") # 讀入 field text_field = torch.load(FIELDFILE_ROOT + 'field/text_field.pth') # 情緒種類 classification = ['sadness', 'joy', 'love', 'anger', 'fear', 'surprise'] # 讀入新、舊 model def datasetNum() : datasetNumList = [0, 0, 0, 0, 0, 0] with open(PROJECT_ROOT + 'train.csv', newline='') as csvfile : reader = csv.reader( csvfile ) dataList = list( reader ) for r in dataList : if r[1] == '0' : datasetNumList[0] = datasetNumList[0] + 1 elif r[1] == '1' : datasetNumList[1] = datasetNumList[1] + 1 elif r[1] == '2' : datasetNumList[2] = datasetNumList[2] + 1 elif r[1] == '3' : datasetNumList[3] = datasetNumList[3] + 1 elif r[1] == '4' : datasetNumList[4] = datasetNumList[4] + 1 elif r[1] == '5' : datasetNumList[5] = datasetNumList[5] + 1 csvfile.close() return datasetNumList def datasetPercentage( datasetNumList ) : datasetNumPercentage = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] all = 0 for i in range(6) : all = datasetNumList[i] + all for i in range(6) : datasetNumPercentage[i] = format( datasetNumList[i] / all, '.2f' ) return datasetNumPercentage def input(request) : return render(request, 'inference/input.html', {}) def result(request) : old_model = MyLSTM().to(device) load_model(PROJECT_ROOT + 'original_model.pt', old_model) new_model = MyLSTM().to(device) load_model(PROJECT_ROOT + 'model.pt', new_model) i = inf() s = Sentence() vocab = text_field.vocab s.input_text = request.POST.get('input_text', False) old_model_ans_int, s.old_probability_list = i.inference(s.input_text, old_model, vocab) new_model_ans_int, s.new_probability_list = i.inference(s.input_text, new_model, vocab) s.old_model_answer = classification[old_model_ans_int] s.new_model_answer = classification[new_model_ans_int] s.fix_answer = s.new_model_answer s.save() datasetNumList = datasetNum() datasetNumPercentage = datasetPercentage( datasetNumList ) content = { 'input_text' : s.input_text, 'new_model_ans' : s.new_model_answer, 'new_model_ans_int' : new_model_ans_int, 'new_probability_list' : s.new_probability_list, 'old_model_ans' : s.old_model_answer, 'old_model_ans_int' : old_model_ans_int, 'old_probability_list' : s.old_probability_list, 'classification' : classification, 'datasetNumList' : datasetNumList, 'datasetNumPercentage' : datasetNumPercentage, } return render(request, 'inference/result.html', context=content) ``` 當前文件夾結構如下 ``` . ├── db.sqlite3 ├── inference │   . │   . │   ├── path.py ✔️ │   └── views.py ✔️ │ └── .. 10 directories, 33 files ``` ## Admin 客製化 修改 ./inference/admin.py ```python= import os from django.contrib import admin from django.http import HttpResponse from .models import * from .path import PROJECT_ROOT import csv import pandas as pd def input_text_name( Sentence ) : return ("%s" %(Sentence.input_text)) input_text_name.short_description = 'Input text' def new_model_ans_name( Sentence ) : return ("%s" %(Sentence.new_model_answer)) new_model_ans_name.short_description = 'Emotion' def fix_ans_name( Sentence ) : return ("%s" %(Sentence.fix_answer)) fix_ans_name.short_description = 'Fix answer ' class ExportCsv : def export_as_csv(self, request, queryset) : meta = self.model._meta data_field_names = ['input_text', 'fix_answer'] newDataList = [] for obj in queryset : newDataList.append([getattr(obj, field) for field in data_field_names] ) classification = ['sadness', 'joy', 'love', 'anger', 'fear', 'surprise'] for i in range( len(newDataList) ) : if ( newDataList[i][1] == classification[0] ) : newDataList[i][1] = int(0) elif ( newDataList[i][1] == classification[1] ) : newDataList[i][1] = int(1) elif ( newDataList[i][1] == classification[2] ) : newDataList[i][1] = int(2) elif ( newDataList[i][1] == classification[3] ) : newDataList[i][1] = int(3) elif ( newDataList[i][1] == classification[4] ) : newDataList[i][1] = int(4) elif ( newDataList[i][1] == classification[5] ) : newDataList[i][1] = int(5) with open(PROJECT_ROOT + 'train.csv', mode = 'a', newline='') as csvfile : writer = csv.writer( csvfile ) for i in newDataList : row = writer.writerow( i ) csvfile.close() export_as_csv.short_description = "將勾選資料與本地端資料集合併" class SentenceAdmin(admin.ModelAdmin, ExportCsv) : list_display = (input_text_name, new_model_ans_name, fix_ans_name) actions = ["export_as_csv"] admin.site.register(Sentence, SentenceAdmin) ``` ## 啟動 Web Server ``` python3 manage.py runserver ``` ``` Watching for file changes with StatReloader Performing system checks... System check identified no issues (0 silenced). January 01, 2023 - 15:57:09 Django version 4.1.3, using settings 'web_demo_tutorial.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C. ``` 開啟瀏覽器,輸入 http://127.0.0.1:8000/ 畫面如下 ![](https://i.imgur.com/bdjDpKP.png) ## 註冊 Admin SuperUser ``` python3 manage.py createsuperuser ``` ``` Username (leave blank to use 'admin'): admin Email address: [INPUTYOUREMAIL] Password: [INPUTYOURPASSWORD] Password (again): [INPUTYOURPASSWORD] Superuser created successfully. ``` 啟動 Web Server ``` python3 manage.py runserver ``` 開啟瀏覽器,輸入 http://127.0.0.1:8000/admin/ 畫面如下 ![](https://i.imgur.com/y8xiCpf.png) 輸入設定的帳號密碼後即可進入管理介面。 ![](https://i.imgur.com/tVYlIrW.png) 點選 Sentences 欄位,可查看過去使用者的輸入紀錄。 ![](https://i.imgur.com/Zc5Kxco.png)