# 透過 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/
畫面如下

## 註冊 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/
畫面如下

輸入設定的帳號密碼後即可進入管理介面。

點選 Sentences 欄位,可查看過去使用者的輸入紀錄。
